Compare commits

..
200 Commits
Author SHA1 Message Date
Diego Waxemberg 83694f8141 Bumping version for v2.6.0 2019-04-29 19:20:46 -07:00
Cameron Gutman a4352aae4d Update common-c to pick up recent performance and bandwidth improvements 2019-04-27 15:01:40 -07:00
Cameron Gutman 2ac19336d6 Rename Multi-Controller Off to Single 2019-04-27 14:25:58 -07:00
Cameron Gutman 63b09c5b27 Remove the old message callbacks 2019-04-27 14:00:58 -07:00
Andrew Scagnelli b2c2f0837a more appletv tweaks (#351)
* redo now playing indicator

- instead of drawing an arrow overlay, add an offset green shadow

* do not show "Connection Terminated" on code 0

if an application quits successfully because it was asked to do so by
the user (errorCode = 0), then do not display an alert -- just go back
to the prior view.

* fix: add missing toggle for mutli-controller support
2019-02-13 21:03:09 -08:00
Cameron Gutman 335b5aef1f Add rumble support 2019-02-11 19:22:18 -08:00
Cameron Gutman 918e1248f2 Store GCController in Controller object to allow us to fetch controller attributes later 2019-02-11 18:21:04 -08:00
Cameron Gutman 442a791e0c Fix SOPS issues when streaming at 120 FPS 2019-02-09 21:03:48 -08:00
Cameron Gutman 573b93329d Merge pull request #354 from eraser2021999/master
Simple solution to solve focus issue on Apple TV.
2019-01-04 20:27:43 -08:00
Cameron Gutman 40359c5e32 Use stun.moonlight-stream.org for STUN 2019-01-04 20:23:46 -08:00
Cameron Gutman 3b01b50503 Update common-c with strict bitrate fix 2019-01-04 20:19:34 -08:00
Bogdan Coticopol 76b55cc230 Simple solution to solve focus issue on Apple TV. 2019-01-02 00:16:26 +02:00
Diego Waxemberg e136cd96c1 Bumping app version for 2.5.0 2018-12-29 10:32:02 -05:00
Cameron Gutman 5028eaed59 Fix crash presenting the loading frame while old one is still dismissing 2018-12-28 20:51:29 -08:00
Cameron Gutman d18c713684 Fix tip display on iPhone SE 2018-12-28 20:32:22 -08:00
Cameron Gutman 08e169eb23 Rewrite loading frame management (again) 2018-12-28 16:45:05 -08:00
Cameron Gutman 611358e35c Fix pairing code that didn't always call one of the pairing completion callbacks 2018-12-28 15:50:13 -08:00
Cameron Gutman 63275130bb Fix duplicate connection termination attempt due to race condition after calling LiInterruptConnection() 2018-12-28 15:31:18 -08:00
Cameron Gutman 7c9313e04d Ensure we don't deadlock if somehow a loading frame ends up under the active view 2018-12-28 02:09:57 -08:00
Cameron Gutman d466a0af07 Only cleanup the stream on termination 2018-12-28 02:03:21 -08:00
Cameron Gutman c8adc184c9 Use a semaphore to ensure the loading frame is safely shown and dismissed 2018-12-28 02:01:40 -08:00
Cameron Gutman d40a29df71 Ensure we can't enter the app grid without having a pinned cert 2018-12-28 01:32:53 -08:00
Cameron Gutman 85820bf812 Use the real online status rather than always assuming YES 2018-12-28 01:24:18 -08:00
Cameron Gutman 5d4d10ac8a Fix displaying GFE error messages for launch and resume 2018-12-28 01:16:56 -08:00
Cameron Gutman eca018ad15 Set host online immediately after using Add PC dialog to update host address 2018-12-28 00:54:26 -08:00
Cameron Gutman 90e582a263 Assert only in the cert untrusted error path 2018-12-28 00:45:32 -08:00
Cameron Gutman 3717228e28 Fix app grid running indicator not being correct immediately after returning from stream 2018-12-28 00:32:19 -08:00
Cameron Gutman 8c0997ddcd Update common-c to fix GFE 3.16 bitrate issue 2018-12-28 00:14:04 -08:00
Cameron Gutman d722c9e18d Use default auth handling if the pinning fails 2018-12-27 21:07:17 -08:00
Cameron Gutman 94c9433b99 Fix running state refresh on app grid 2018-12-27 19:54:27 -08:00
Cameron Gutman befef62b2f Log parsed XML attributes 2018-12-27 19:50:51 -08:00
Cameron Gutman dc00c1e3e7 Fix leak of UIComputerViews that poll forever 2018-12-27 19:50:29 -08:00
Cameron Gutman 3555d60a9b Add a quit tip for iOS and tvOS 2018-12-22 21:43:37 -08:00
Cameron Gutman bd8b9f8bfb Use a shared UID for all Moonlight clients 2018-12-22 21:02:24 -08:00
Cameron Gutman 72dcd76e37 Require cert pinning for HTTPS 2018-12-22 20:39:13 -08:00
Cameron Gutman fbae7f88b5 Add server cert pinning after pairing 2018-12-22 00:05:48 -08:00
Cameron Gutman 791a5b1ea1 Update project with Xcode recommendations 2018-12-07 21:48:54 -08:00
Cameron Gutman 8c5a82573b Update readme to new URL 2018-12-03 21:43:19 -08:00
Cameron Gutman cc56a36bbd Properly set GCSupportedGameControllers and GCSupportsControllerUserInteraction plist values 2018-12-01 15:00:24 -08:00
Cameron Gutman 336dbeed98 Remove loading frame from Add Host dialog flow 2018-11-28 18:43:45 -08:00
Cameron Gutman f8613cdbfd Fix broken app and host taps on iOS 8 2018-11-28 18:43:20 -08:00
Cameron Gutman a5961dd9b5 Use lower MTU for remote streaming and higher MTU for local networks 2018-11-21 21:03:08 -08:00
Cameron Gutman 17a5866fdd Merge pull request #341 from ascagnel/tv-fixes
tvOS app UI/UX fixes
2018-11-19 20:16:50 -08:00
Diego Waxemberg 5e77380636 Bumping app version for 2.4.0 2018-11-17 16:00:51 -08:00
Cameron Gutman 845af246b5 Fix a few cases where the loading frame could be left on-screen 2018-11-16 20:17:03 -08:00
Cameron Gutman 87cac4e562 Fix host long press causing duplicate menus to show up 2018-11-16 20:05:06 -08:00
Cameron Gutman 70eb4fd69d Allow waking non-paired hosts 2018-11-16 19:30:05 -08:00
Cameron Gutman c5120bec37 Remove wait between retries after polling failure 2018-11-16 19:14:00 -08:00
Cameron Gutman 24fc9ac0a9 Change "host already added" message to be more clear 2018-11-16 19:03:23 -08:00
Cameron Gutman 2c0624f1e7 Improve handling of non-local addresses and remote streaming 2018-11-16 19:02:31 -08:00
Cameron Gutman 8b227f28f7 Update common-c to fix NAT64 2018-11-16 00:41:23 -08:00
Cameron Gutman 23e9c3f236 Fix crash when the loading spinner is dismissed before fully presenting 2018-11-15 22:02:05 -08:00
Cameron Gutman eefde316de Fix bug that can cause spinner to get stuck 2018-11-15 21:11:02 -08:00
Cameron Gutman a3cfd787de Update common-c for GFE 3.16 2018-11-15 13:37:28 -08:00
Cameron Gutman 410d0d367b Fix handling of missing L3 and R3 buttons and adjust on-screen controls based on L3/R3 presence 2018-11-03 14:01:32 -07:00
Cameron Gutman 39b04d46a4 Add support for L3 and R3 on iOS 12.1 2018-10-31 18:08:34 -07:00
Andrew Scagnelli 0e8647ad07 feat: added settings bundle 2018-10-28 16:23:24 -04:00
Diego Waxemberg 73091e7e8c Creating build for 2.3.0 2018-10-27 12:55:02 -07:00
Cameron Gutman 16a2ab3941 Fix settings not saving on the first stream if the settings slideout is not closed 2018-10-27 11:57:29 -07:00
Cameron Gutman 39a2e8be78 Log an error if STUN fails 2018-10-26 20:51:31 -07:00
Cameron Gutman 8e6dadfa79 Use STUN to get the WAN address for PCs discovered by mDNS 2018-10-26 20:28:59 -07:00
Andrew Scagnelli c2134585df fix: apply tvos style to missing icon 2018-10-24 00:08:06 -04:00
Andrew Scagnelli dedcbd9d3e feat: update text color
do not apply UIComputerView text color so it uses the theme default
2018-10-23 23:28:07 -04:00
Andrew Scagnelli bd379677a2 feat: updated menu button handling
- on tapping menu once (or chording with LB/RB), behave as
start/select/special
- on doube-tapping menu, quit the stream
2018-10-23 23:18:56 -04:00
Andrew Scagnelli 466ebe2840 disable focus effects on overlay icon 2018-10-22 23:45:17 -04:00
Cameron Gutman 736887b945 Add 120 FPS option for devices with high refresh rate displays 2018-10-22 20:24:51 -07:00
Cameron Gutman 993bfca0ae Allow bitrate slider to go to 100 Mbps 2018-10-22 19:27:49 -07:00
Cameron Gutman 5ad87f7400 Hide the textbox so it doesn't obstruct normal UI controls. Fixes #344. 2018-10-22 19:06:52 -07:00
Andrew Scagnelli e862dbd6b6 adding some basic placeholder icons 2018-10-19 01:05:48 -04:00
Andrew Scagnelli befde5eb28 system theme, tvos icon styling
- using tvos icon styling
- updated to use system theme (light/dark) instead of always being grey
2018-10-19 00:03:14 -04:00
Andrew Scagnelli 7fd3b8df1c on tvOS, do not quit the stream on pressing B
on tvOS (maybe also on other cases where you're using a "real" gamepad?),
use GCEventViewController so the system-level "back" event doesn't quit
the stream
2018-10-18 00:04:04 -04:00
Diego Waxemberg cee501d9b4 Bumping app version for 2.2.0 2018-10-08 21:14:18 -07:00
Cameron Gutman a103c0fd45 Update common-c to use split frame encode on HEVC 2018-10-08 20:26:32 -07:00
Cameron Gutman bf71ae003f Add all modifier flag combinations 2018-10-08 20:25:43 -07:00
Cameron Gutman 5b102ce3d3 Merge pull request #335 from tranchis/ios-keyboard
Improve keyboard support
2018-09-27 21:44:11 -07:00
Sergio Alvarez-Napagao c2012fa195 Improve keyboard support 2018-08-30 21:06:36 +02:00
Cameron Gutman 003fc8c1f8 Improve keyboard handling for unexpected inputs and multi-character input (such as dictation) 2018-08-29 10:02:58 -07:00
Cameron Gutman 9123033bb3 Remove help links from tvOS version due to lack of browser and WebKit 2018-08-29 09:36:51 -07:00
Cameron Gutman 253144188c Purge box art cache on low memory warning 2018-08-29 09:24:47 -07:00
Cameron Gutman 4ccb9bebbf Fix stream cleanup when pressing Menu on tvOS 2018-08-29 09:21:49 -07:00
Cameron Gutman 49f647748c Fix discovery being stopped after returning from Add PC dialog 2018-08-29 07:52:33 -07:00
Cameron Gutman e2e8f0121d Persist the database in NSUserDefaults on tvOS 2018-08-28 17:42:59 -07:00
Cameron Gutman b5ea4c3e50 Change URL timeout to 4 seconds to improve offline behavior 2018-08-27 01:55:34 -07:00
Cameron Gutman 90d47004e7 Avoid storing images in the database 2018-08-27 01:54:58 -07:00
Cameron Gutman 450960eaaf Fix credential storage on tvOS 2018-08-27 00:34:34 -07:00
Diego Waxemberg 44550baaa9 Changing directories for storage on tvOS because tvOS does not allow permanent storage 2018-08-26 21:54:49 -07:00
Cameron Gutman 293dc26b2e Fix the navigation bar look 2018-08-26 19:03:22 -07:00
Cameron Gutman 109fa6f463 Set the title of the view to the selected PC 2018-08-26 17:05:13 -07:00
Cameron Gutman 44f64e1a35 Fix several UI issues 2018-08-26 16:43:45 -07:00
Cameron Gutman 2e479837ed Fix spinners on tvOS and mouse support using the remote 2018-08-26 15:55:39 -07:00
Cameron Gutman 1b681a8730 Allow tvOS to back out from app grid to PC grid 2018-08-26 15:05:32 -07:00
Cameron Gutman f988ce0278 Fix issues with the loading frame colliding with other views 2018-08-26 12:25:22 -07:00
Cameron Gutman 1ef5c87afc Share MainFrameViewController between iOS and tvOS 2018-08-26 11:56:27 -07:00
Diego Waxemberg f5b99b6b9c Can now stream to tvOS 2018-08-26 11:31:20 -07:00
Diego Waxemberg 485dd4b001 Merge branch 'tvOS' of github.com:moonlight-stream/moonlight-ios into tvOS 2018-08-26 11:17:20 -07:00
Diego Waxemberg 92e2c0d7cc app cells are now clickable in tvOS 2018-08-26 11:14:15 -07:00
Cameron Gutman 48ffca57ec Fix infinite loop of loading UI spinners 2018-08-26 11:09:15 -07:00
Cameron Gutman e83a996cd5 Re-port MainFrameViewController to tvOS to fix loading page issues 2018-08-26 10:59:31 -07:00
Diego Waxemberg 57191e3b9a added a highlight state for focused views (can now see what's selected in tvOS) 2018-08-26 10:53:28 -07:00
Diego Waxemberg eadcf3b3eb fixed 'unsafe' requests and can now click stuff 2018-08-26 01:15:53 -07:00
Cameron Gutman 71e0646659 Add code to handle discovery and game controllers 2018-08-26 00:48:29 -07:00
Diego Waxemberg f2edee1669 Merge branch 'tvOS' of github.com:moonlight-stream/moonlight-ios into tvOS 2018-08-26 00:33:43 -07:00
Diego Waxemberg 11948bf618 wip 2018-08-26 00:15:56 -07:00
Cameron Gutman 4a9294a4b7 Remove Roboto fonts, switch to iOS app delegate for tvOS, and add stream view and loading view 2018-08-26 00:14:08 -07:00
Cameron Gutman a9b8f753a2 Remove tvOS-specific data model 2018-08-25 23:32:43 -07:00
Diego Waxemberg 05761c787c working on tvOS UI 2018-08-25 23:22:52 -07:00
Cameron Gutman 9a7b3b1325 Build libopus for tvOS and iOS with bitcode 2018-08-25 21:17:08 -07:00
Cameron Gutman b04a4e72ff Rebuild OpenSSL with bitcode for macOS, iOS, and tvOS 2018-08-25 20:34:36 -07:00
Diego Waxemberg b0b9a614f3 Adding tvOS target 2018-08-25 20:33:03 -07:00
Diego Waxemberg 76ab786e94 Adding keyboard support 2018-08-25 15:22:32 -07:00
Cameron Gutman 8afcdc92d4 Update common-c to fix GFE 3.14.1 audio bitrate issues 2018-08-25 00:32:33 -07:00
Cameron Gutman 907a2cd59b Update README.md 2018-08-24 23:20:16 -07:00
Cameron Gutman a85b931d87 Enable stale and no-response bots to reduce inactive issues 2018-08-12 00:22:30 -07:00
Diego Waxemberg 62f1b3e595 Bumping app version for 2.1.0 2018-06-17 16:43:42 -07:00
Cameron Gutman b9381ad027 Update common-c to fix RTSP handshake error on quick reconnect 2018-06-09 21:54:25 -07:00
Cameron Gutman 98d01f7775 Ensure the C strings stay valid until connection-time. Fixes #228 2018-06-09 20:04:40 -07:00
Cameron Gutman de39c87185 Allow the stream to be inactive for 10 seconds before terminating 2018-06-09 16:29:48 -07:00
Cameron Gutman 10a00b0d28 Avoid leaving application lifetime notifications registered all the time 2018-06-09 16:28:37 -07:00
Diego Waxemberg 4d8788add3 Bumping app version for 2.0.0 2018-06-02 21:31:53 -07:00
Cameron Gutman 9c019be322 Fix subtle database corruption that would cause games common to 2 or more PCs to jump between them based on the last app list fetched 2018-06-02 20:44:12 -07:00
Cameron Gutman 80254513c4 Avoid blocking the main thread when delaying for button presses 2018-06-02 19:19:24 -07:00
Cameron Gutman 5a13e44fdb Use an exponential-ish bitrate slider 2018-06-02 19:09:38 -07:00
Cameron Gutman ec0a462092 Remove custom style on UISegmentedControl, since the default actually looks better 2018-06-02 18:14:20 -07:00
Cameron Gutman 25c7b18147 Add UI settings for HEVC, optimizing games, PC audio, multi-controller. Remove remote setting since it doesn't do much and might confuse users. 2018-06-02 18:13:32 -07:00
Cameron Gutman fb238e0d1d Log launch and resume requests 2018-06-02 17:50:48 -07:00
Cameron Gutman bfc0b89926 Add many new preferences to settings DB. Default HEVC to off due to reference frame invalidation not working 2018-06-02 17:10:52 -07:00
Cameron Gutman 2f0e219726 Always set the stream config from the data from the actual app we're going to start streaming, not just the one selected 2018-06-02 16:38:02 -07:00
Cameron Gutman 2305f33572 Fix scrolling jank on app list 2018-06-02 16:31:16 -07:00
Cameron Gutman d17f2f9dee Plumb multi-controller toggle 2018-06-02 13:52:58 -07:00
Cameron Gutman a28049a58c Plumb several new options into stream config and launch requests: optimizeGameSettings, playAudioOnPC, surround sound, and HDR 2018-06-02 13:25:53 -07:00
Cameron Gutman 2d2f71dcab Parse and persist ServerCodecModeSupport attribute for hosts 2018-06-02 13:10:10 -07:00
Cameron Gutman 59506a2364 Parse and remember the IsHdrSupported app attribute 2018-06-02 12:33:13 -07:00
Cameron Gutman 2cec53ab6b Fix typo in analog stick deadzone area 2018-06-02 01:38:52 -07:00
Cameron Gutman 42ef67d939 Let the spinner spin until video appears 2018-06-02 01:02:10 -07:00
Cameron Gutman 30ec68cd4e Ensure the progress messages aren't obscured by the video layer 2018-06-02 01:01:45 -07:00
Cameron Gutman 941c414930 Fix R2 being improperly sized on 3x DPI 2018-06-01 22:20:37 -07:00
Cameron Gutman b918005555 Improve OSC usability for iPad. Fixes #317 2018-06-01 22:09:24 -07:00
Cameron Gutman c74fa4d5b0 Change manual simple OSC mode to have useful buttons. Fixes #218 2018-06-01 21:18:41 -07:00
Cameron Gutman 1a71e040c0 Fix deadzones to dynamically detect controls on screen and add a deadzone for the analog sticks 2018-06-01 21:17:47 -07:00
Cameron Gutman 236c9d5c6e Use more aggressive mDNS querying for faster PC discovery, particularly in newly online PC scenarios 2018-06-01 20:23:39 -07:00
Cameron Gutman 862b8632e3 Fix DiscoveryManager trying to start discovery again on all machines when any machine is discovered 2018-06-01 19:50:32 -07:00
Cameron Gutman cdb5f51ab8 Quiet down PC discovery logging 2018-06-01 19:38:52 -07:00
Cameron Gutman 314f32388a Use the app name in the "Starting App" text 2018-06-01 19:11:10 -07:00
Cameron Gutman 39b0b429d6 Show loading spinner while waiting for apps to quit 2018-06-01 18:58:25 -07:00
Cameron Gutman 23a12ed101 Actually set CAPABILITY_DIRECT_SUBMIT and fix the bug it hides 2018-06-01 17:00:02 -07:00
Cameron Gutman 653a789110 Refresh app and host state when returning from streaming via error 2018-06-01 00:47:44 -07:00
Cameron Gutman 04b2fccaef Only interact with the AVSampleBufferDisplayLayer on the main thread 2018-06-01 00:31:41 -07:00
Cameron Gutman ec0124e255 Ensure we properly restart discovery after closing Control Center 2018-06-01 00:07:25 -07:00
Cameron Gutman 132a1fc410 Don't attempt to pair on entry to foreground if we've become unpaired 2018-05-31 01:53:09 -07:00
Cameron Gutman 63297c8483 Lan -> LAN 2018-05-31 01:37:13 -07:00
Cameron Gutman 5fd2c29a73 Avoid displaying error dialogs on passive actions like returning to foreground 2018-05-31 01:36:10 -07:00
Cameron Gutman e6e7ec9202 Avoid animating the loading frame 2018-05-31 01:22:03 -07:00
Cameron Gutman fcc770d820 Avoid firing off HTTP requests as we enter the background when entering home from streaming 2018-05-31 00:46:41 -07:00
Cameron Gutman 4243bcd5d1 Ensure double-starting or stopping DiscoveryManager and MDNSManager doesn't cause side-effects, since we do this when Home is pressed while streaming 2018-05-31 00:33:24 -07:00
Cameron Gutman 67b08cd8b9 Use LiInterruptConnection() to terminate connection attempts more quickly 2018-05-30 22:06:20 -07:00
Cameron Gutman e697938afc Re-enable the idle timer when streaming has ended 2018-05-30 22:04:06 -07:00
Cameron Gutman 275f6df8c1 Fix some minor issues with saving CoreData objects 2018-05-30 01:19:46 -07:00
Cameron Gutman aea2067e1c Silence the firehose of debug logging statements 2018-05-29 20:17:48 -07:00
Cameron Gutman b5241a0c30 Ok -> OK 2018-05-28 18:20:59 -07:00
Cameron Gutman 6cac2348b4 Add help button to error dialogs and use default style for non-destructive actions 2018-05-28 18:00:19 -07:00
Cameron Gutman 0293df7748 Rewrite audio playback to simplify code, reduce allocations, and eliminate priority inversion on the queue lock. This completely eliminates clicks and pops in audio for me on my iPhone SE and the emulator. 2018-05-28 17:38:09 -07:00
Cameron Gutman 391872ddb8 Ignore unsupported gamepads 2018-05-28 15:11:23 -07:00
Cameron Gutman 4bafefbe16 streamingRemotely should not be optional 2018-05-28 13:51:00 -07:00
Cameron Gutman 812f3f3717 Add 360p resolution support 2018-05-28 13:50:20 -07:00
Cameron Gutman e498736af6 Fix Moonlight macOS build 2018-05-28 13:25:18 -07:00
Cameron Gutman 1842a11f56 Fix errors saving PCs that were discovered via mDNS 2018-05-28 13:19:49 -07:00
Cameron Gutman 85f81ca8c1 Use CoreData codegen to resolve warnings about missing getters/setters on our DB objects 2018-05-28 12:40:15 -07:00
Cameron Gutman b792629943 Flip the pull arrow when the settings are open 2018-05-28 12:04:38 -07:00
Cameron Gutman 7df76fcd93 Allow the settings view to scroll on smaller iPhones 2018-05-28 11:39:04 -07:00
Cameron Gutman 8b60e79406 Minor common-c update to address a (currently) harmless warning 2018-05-28 10:49:41 -07:00
Cameron Gutman fcb6c7386c Update common-c to receive fixes to lower audio bitrate at low bandwidth settings 2018-05-28 10:36:17 -07:00
Cameron Gutman 2729fad47c Commit IDEWorkspaceChecks.plist 2018-05-28 10:35:36 -07:00
Cameron Gutman 41eb933e11 Update to XCode 9.3 recommended settings and fix "implict retain self" warnings 2018-04-21 22:20:27 -07:00
Felix Kratz f759f719e6 Dark Mode & Stream Overlay [macOS] (#315)
* dark mode & stream overlay

* removed all redundant imports

* update for the new xcode version with fixes for the new 'implicitly retains self warning'

* reworked the overlay view

* cleaning up unused variables

* small corrections
2018-04-22 00:44:22 -04:00
Cameron Gutman 74283a6763 Use HEVC on iOS 11.3 and later. Fixes #296 2018-04-10 18:57:02 -07:00
Cameron Gutman 0a19c5950a Update organization name in both projects 2018-03-27 00:38:47 -07:00
Cameron Gutman e8832ed746 Refactor new macOS code to reduce #ifs, increase shared code, and fix warnings 2018-03-27 00:34:38 -07:00
Felix Kratz 6cc165b589 Port for macOS (#311)
* merged moonlight-mac with moonlight-ios

* reverted to the original project.pbxproj

* cleaned up the code, fixed lots of unnecessary code duplications

* multicontroller support (not tested)

* new class that can be used for further modularization of the MainFrameViewController
2018-03-26 23:50:40 -07:00
Cameron Gutman 1c86c4485d Disable HEVC until iPhone X and other issues can be sorted out. See #296 2018-02-24 21:16:02 -08:00
Cameron Gutman 127e5ed54a Migrate to Swift 4.0 with @objc inferencing fixes for Moonlight build target 2018-02-19 14:43:50 -08:00
Cameron Gutman 3fee5d0a62 Fix build of unit tests 2018-02-19 14:33:20 -08:00
Cameron Gutman c913b3f04f Enable XCode 9 recommended warnings and fix occurrences of them in the code 2018-02-19 14:26:12 -08:00
Cameron Gutman dfd30c0624 Remove some old and unused references from the XCode project 2018-02-19 14:05:03 -08:00
Cameron Gutman 5117a6ede5 Replace dylibs with tbd 2018-02-19 13:56:50 -08:00
Cameron Gutman fb9c071243 Update moonlight-common-c 2018-02-19 00:25:57 -08:00
Cameron Gutman d0a8f0e0f1 Update README.md 2018-01-20 17:03:09 -08:00
Cameron Gutman 6bb6c40bec Report attached gamepads during launch to fix L4D2 2018-01-20 14:37:33 -08:00
Cameron Gutman af3bc82d0f Fix drag cancellation triggering on any movement and other drag bugs. Fixes #288 2018-01-20 01:51:50 -08:00
Cameron Gutman b9d5d94d70 Fix support for IPv6 literals 2018-01-20 01:30:29 -08:00
Cameron Gutman 43f4804194 Fix wake-on-lan failure on LAN after ARP cache expiration of target PC. Fixes #293 2018-01-19 20:18:39 -08:00
Diego Waxemberg be26ce9a8f Bumping app version for v1.7.0 2018-01-10 19:35:41 -08:00
Cameron Gutman ca562e2251 Update common-c to fix streaming on GFE 3.12 2018-01-10 00:45:38 -08:00
Cameron Gutman d7ee0916e9 Avoid crashing on iOS 11 betas released before iPhone X 2017-12-27 23:24:53 -08:00
Cameron Gutman 3bc9e050f1 Update common-c and use codec masks rather than exact formats 2017-12-03 11:33:11 -08:00
Cameron Gutman 4b9bd8398b Update for video decoder API changes 2017-11-18 18:17:22 -08:00
Diego Waxemberg 20a61dd9f4 Bumping app version for v1.6.0 2017-11-12 19:57:36 -08:00
Cameron Gutman dc4ecbf262 Update common-c to address HEVC corruption at 1080p60 2017-11-10 01:36:00 -08:00
Diego Waxemberg 68f55235ec Bumping app version for v1.5.0 2017-10-17 21:28:30 -07:00
Cameron Gutman 5434b653c7 Reduce bitrate when streaming HEVC to reduce bandwidth at better IQ than H.264 2017-10-17 01:07:41 -07:00
Cameron Gutman 25d7af9453 Fix pairing and game quitting issues with GFE 3.10 2017-10-16 23:59:00 -07:00
281 changed files with 34702 additions and 25023 deletions
+8
View File
@@ -0,0 +1,8 @@
# ProBot No Response (https://probot.github.io/apps/no-response/)
daysUntilClose: 7
responseRequiredLabel: 'need more info'
closeComment: >
This issue has been automatically closed because there was no response to a
request for more information from the issue opener. Please leave a comment or
open a new issue if you have additional information related to this issue.
+14
View File
@@ -0,0 +1,14 @@
# ProBot Stale (https://probot.github.io/apps/stale/)
daysUntilStale: 90
daysUntilClose: 7
exemptLabels:
- accepted
- bug
- enhancement
- meta
staleLabel: stale
markComment: >
This issue has been automatically marked as stale because it has not had
recent activity. It will be closed if no further activity occurs.
closeComment: false
+8
View File
@@ -6,11 +6,19 @@
// 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 (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
+94 -38
View File
@@ -16,35 +16,17 @@
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";
#endif
#if TARGET_OS_IPHONE
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
[[UILabel appearance] setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
[[UIButton appearance].titleLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
// Generate selected segment background image
CGSize borderImageSize = CGSizeMake(1.f, 100.f);
UIGraphicsBeginImageContext(borderImageSize);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextFillRect(context, CGRectMake(0.f, borderImageSize.height * 0.8, borderImageSize.width, borderImageSize.height));
UIImage *selectedSegmentBG = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
// Clear default border and background color
[[UISegmentedControl appearance] setBackgroundImage:[[UIImage alloc] init] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
[[UISegmentedControl appearance] setDividerImage:[[UIImage alloc] init] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
// Set selected segment background image
[[UISegmentedControl appearance] setBackgroundImage:[selectedSegmentBG imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
// Change font on UISegmentedControl
[[UISegmentedControl appearance] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
[UIColor whiteColor], NSForegroundColorAttributeName,
[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]], NSFontAttributeName, nil] forState:UIControlStateNormal];
return YES;
}
@@ -57,7 +39,7 @@ static NSOperationQueue* mainQueue;
- (void)applicationDidEnterBackground:(UIApplication *)application
{
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
}
@@ -76,15 +58,36 @@ static NSOperationQueue* mainQueue;
// 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
{
NSError *error = nil;
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
if (managedObjectContext != nil) {
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
}
[managedObjectContext performBlock:^{
if (![managedObjectContext hasChanges]) {
return;
}
NSError *error = nil;
if (![managedObjectContext save:&error]) {
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
}
#if TARGET_OS_TV
NSData* dbData = [NSData dataWithContentsOfURL:[[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME]];
[[NSUserDefaults standardUserDefaults] setObject:dbData forKey:DB_NAME];
#endif
}];
}
}
@@ -125,19 +128,30 @@ static NSOperationQueue* mainQueue;
return _persistentStoreCoordinator;
}
NSURL *storeURL = [self getStoreURL];
NSError *error = nil;
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
NSString* storeType;
#if TARGET_OS_TV
// Use a binary store for tvOS since we will need exclusive access to the file
// to serialize into NSUserDefaults.
storeType = NSBinaryStoreType;
#else
storeType = NSSQLiteStoreType;
#endif
// We must ensure the persistent store is ready to opened
[self preparePersistentStore];
if (![_persistentStoreCoordinator addPersistentStoreWithType:storeType configuration:nil URL:[self getStoreURL] options:options error:&error]) {
// Log the error
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
// Drop the database
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
[self dropDatabase];
// Try again
return [self persistentStoreCoordinator];
@@ -154,8 +168,50 @@ static NSOperationQueue* mainQueue;
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
}
- (void) dropDatabase
{
// Delete the file on disk
[[NSFileManager defaultManager] removeItemAtURL:[self getStoreURL] error:nil];
#if TARGET_OS_TV
// Also delete the copy in the NSUserDefaults on tvOS
[[NSUserDefaults standardUserDefaults] removeObjectForKey:DB_NAME];
#endif
}
- (void) preparePersistentStore
{
#if TARGET_OS_TV
// On tvOS, we may need to inflate the DB from NSUserDefaults
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *cacheDirectory = [paths objectAtIndex:0];
NSString *dbPath = [cacheDirectory stringByAppendingPathComponent:DB_NAME];
// Always prefer the on disk version
if (![[NSFileManager defaultManager] fileExistsAtPath:dbPath]) {
// If that is unavailable, inflate it from NSUserDefaults
NSData* data = [[NSUserDefaults standardUserDefaults] dataForKey:DB_NAME];
if (data != nil) {
Log(LOG_I, @"Inflating database from NSUserDefaults");
[data writeToFile:dbPath atomically:YES];
}
else {
Log(LOG_I, @"No database on disk or in NSUserDefaults");
}
}
else {
Log(LOG_I, @"Using cached database");
}
#endif
}
- (NSURL*) getStoreURL {
return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight_iOS.sqlite"];
#if TARGET_OS_TV
// We use the cache folder to store our database on tvOS
return [[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME];
#else
return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:DB_NAME];
#endif
}
@end
+2 -4
View File
@@ -6,16 +6,14 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface CryptoManager : NSObject
+ (void) generateKeyPairUsingSSl;
+ (void) generateKeyPairUsingSSL;
+ (NSData*) readCertFromFile;
+ (NSData*) readKeyFromFile;
+ (NSData*) readP12FromFile;
+ (NSData*) getSignatureFromCert:(NSData*)cert;
+ (NSData*) nullTerminateString:(NSData*)data;
+ (NSData*) pemToDer:(NSData*)pemCertBytes;
- (NSData*) createAESKeyFromSaltSHA1:(NSData*)saltedPIN;
- (NSData*) createAESKeyFromSaltSHA256:(NSData*)saltedPIN;
+94 -46
View File
@@ -87,17 +87,28 @@ static NSData* p12 = nil;
return (((int)[data length] + 15) / 16) * 16;
}
+ (NSData*) nullTerminateString:(NSData*)data {
NSMutableData* mutData = [NSMutableData dataWithData:data];
[mutData appendBytes:"" length:1];
return mutData;
+ (NSData*) pemToDer:(NSData*)pemCertBytes {
X509* x509;
BIO* bio = BIO_new_mem_buf([pemCertBytes bytes], (int)[pemCertBytes length]);
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
bio = BIO_new(BIO_s_mem());
i2d_X509_bio(bio, x509);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
NSData* ret = [[NSData alloc] initWithBytes:mem->data length:mem->length];
BIO_free(bio);
return ret;
}
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert {
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
X509* x509;
BIO* bio = BIO_new(BIO_s_mem());
BIO_puts(bio, buffer);
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
@@ -122,9 +133,7 @@ static NSData* p12 = nil;
}
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
const char* buffer = [[CryptoManager nullTerminateString:key] bytes];
BIO* bio = BIO_new(BIO_s_mem());
BIO_puts(bio, buffer);
BIO* bio = BIO_new_mem_buf([key bytes], (int)[key length]);
EVP_PKEY* pkey;
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
@@ -159,57 +168,60 @@ static NSData* p12 = nil;
return signedData;
}
// TODO: these three methods are almost identical, fix the copy-pasta
+ (NSData*) readCryptoObject:(NSString*)item {
#if TARGET_OS_TV
return [[NSUserDefaults standardUserDefaults] dataForKey:item];
#else
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
return [NSData dataWithContentsOfFile:file];
#endif
}
+ (void) writeCryptoObject:(NSString*)item data:(NSData*)data {
#if TARGET_OS_TV
[[NSUserDefaults standardUserDefaults] setObject:data forKey:item];
#else
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
[data writeToFile:file atomically:NO];
#endif
}
+ (NSData*) readCertFromFile {
if (cert == nil) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
cert = [NSData dataWithContentsOfFile:certFile];
cert = [CryptoManager readCryptoObject:@"client.crt"];
}
return cert;
}
+ (NSData*) readP12FromFile {
if (p12 == nil) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
p12 = [NSData dataWithContentsOfFile:p12File];
p12 = [CryptoManager readCryptoObject:@"client.p12"];
}
return p12;
}
+ (NSData*) readKeyFromFile {
if (key == nil) {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
key = [NSData dataWithContentsOfFile:keyFile];
key = [CryptoManager readCryptoObject:@"client.key"];
}
return key;
}
+ (bool) keyPairExists {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString *documentsDirectory = [paths objectAtIndex:0];
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
bool keyFileExists = [[NSFileManager defaultManager] fileExistsAtPath:keyFile];
bool p12FileExists = [[NSFileManager defaultManager] fileExistsAtPath:p12File];
bool certFileExists = [[NSFileManager defaultManager] fileExistsAtPath:certFile];
bool keyFileExists = [CryptoManager readCryptoObject:@"client.key"] != nil;
bool p12FileExists = [CryptoManager readCryptoObject:@"client.p12"] != nil;
bool certFileExists = [CryptoManager readCryptoObject:@"client.crt"] != nil;
return keyFileExists && p12FileExists && certFileExists;
}
+ (NSData *)getSignatureFromCert:(NSData *)cert {
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
X509* x509;
BIO* bio = BIO_new(BIO_s_mem());
BIO_puts(bio, buffer);
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
if (!x509) {
Log(LOG_E, @"Unable to parse certificate in memory!");
@@ -218,23 +230,59 @@ static NSData* p12 = nil;
return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
}
+ (void) generateKeyPairUsingSSl {
+ (NSData*)getKeyFromCertKeyPair:(CertKeyPair*)certKeyPair {
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_PrivateKey(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
BIO_free(bio);
return data;
}
+ (NSData*)getP12FromCertKeyPair:(CertKeyPair*)certKeyPair {
BIO* bio = BIO_new(BIO_s_mem());
i2d_PKCS12_bio(bio, certKeyPair->p12);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
BIO_free(bio);
return data;
}
+ (NSData*)getCertFromCertKeyPair:(CertKeyPair*)certKeyPair {
BIO* bio = BIO_new(BIO_s_mem());
PEM_write_bio_X509(bio, certKeyPair->x509);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
BIO_free(bio);
return data;
}
+ (void) generateKeyPairUsingSSL {
static dispatch_once_t pred;
dispatch_once(&pred, ^{
if (![CryptoManager keyPairExists]) {
Log(LOG_I, @"Generating Certificate... ");
CertKeyPair certKeyPair = generateCertKeyPair();
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
NSString* documentsDirectory = [paths objectAtIndex:0];
NSString* certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
NSString* keyPairFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
NSString* p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
NSData* certData = [CryptoManager getCertFromCertKeyPair:&certKeyPair];
NSData* p12Data = [CryptoManager getP12FromCertKeyPair:&certKeyPair];
NSData* keyData = [CryptoManager getKeyFromCertKeyPair:&certKeyPair];
//Log(LOG_D, @"Writing cert and key to: \n%@\n%@", certFile, keyPairFile);
saveCertKeyPair([certFile UTF8String], [p12File UTF8String], [keyPairFile UTF8String], certKeyPair);
freeCertKeyPair(certKeyPair);
[CryptoManager writeCryptoObject:@"client.crt" data:certData];
[CryptoManager writeCryptoObject:@"client.p12" data:p12Data];
[CryptoManager writeCryptoObject:@"client.key" data:keyData];
Log(LOG_I, @"Certificate created");
}
});
-2
View File
@@ -6,8 +6,6 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface IdManager : NSObject
+ (NSString*) getUniqueId;
+1 -16
View File
@@ -19,7 +19,7 @@ static const int NUM_YEARS = 10;
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
int add_ext(X509 *cert, int nid, char *value);
struct CertKeyPair generateCertKeyPair() {
struct CertKeyPair generateCertKeyPair(void) {
BIO *bio_err;
X509 *x509 = NULL;
EVP_PKEY *pkey = NULL;
@@ -62,21 +62,6 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
PKCS12_free(certKeyPair.p12);
}
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair) {
FILE* certFilePtr = fopen(certFile, "w");
FILE* keyPairFilePtr = fopen(keyPairFile, "w");
FILE* p12FilePtr = fopen(p12File, "wb");
//TODO: error check
PEM_write_PrivateKey(keyPairFilePtr, certKeyPair.pkey, NULL, NULL, 0, NULL, NULL);
PEM_write_X509(certFilePtr, certKeyPair.x509);
i2d_PKCS12_fp(p12FilePtr, certKeyPair.p12);
fclose(p12FilePtr);
fclose(certFilePtr);
fclose(keyPairFilePtr);
}
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
X509 *x;
EVP_PKEY *pk;
+1 -2
View File
@@ -18,8 +18,7 @@ typedef struct CertKeyPair {
PKCS12 *p12;
} CertKeyPair;
struct CertKeyPair generateCertKeyPair();
struct CertKeyPair generateCertKeyPair(void);
void freeCertKeyPair(CertKeyPair);
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair);
#endif
@@ -1,25 +0,0 @@
//
// App+CoreDataProperties.h
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
// to keep your implementation up to date with your model.
//
#import "App.h"
NS_ASSUME_NONNULL_BEGIN
@interface App (CoreDataProperties)
@property (nullable, nonatomic, retain) NSString *id;
@property (nullable, nonatomic, retain) NSData *image;
@property (nullable, nonatomic, retain) NSString *name;
@property (nullable, nonatomic, retain) Host *host;
@end
NS_ASSUME_NONNULL_END
-18
View File
@@ -1,18 +0,0 @@
//
// App.h
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@class Host;
@interface App : NSManagedObject
@end
#import "App+CoreDataProperties.h"
-13
View File
@@ -1,13 +0,0 @@
//
// App.m
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
#import "App.h"
@implementation App
@end
+11 -3
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "AppDelegate.h"
#import "TemporaryHost.h"
#import "TemporaryApp.h"
@@ -14,12 +13,21 @@
@interface DataManager : NSObject
- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls;
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
framerate:(NSInteger)framerate
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;
- (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host;
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
- (void) updateIconForExistingApp:(TemporaryApp*)app;
- (void) removeHost:(TemporaryHost*)host;
- (void) removeApp:(TemporaryApp*)app;
+29 -28
View File
@@ -9,7 +9,6 @@
#import "DataManager.h"
#import "TemporaryApp.h"
#import "TemporarySettings.h"
#import "Settings.h"
@implementation DataManager {
NSManagedObjectContext *_managedObjectContext;
@@ -22,16 +21,17 @@
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
// Main Thread Checker happy.
if ([NSThread isMainThread]) {
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
}
else {
dispatch_sync(dispatch_get_main_queue(), ^{
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
self->_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
});
}
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
[_managedObjectContext setPersistentStoreCoordinator:_appDelegate.persistentStoreCoordinator];
[_managedObjectContext setParentContext:[_appDelegate managedObjectContext]];
return self;
}
@@ -53,16 +53,31 @@
return uid;
}
- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls {
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
framerate:(NSInteger)framerate
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 {
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
// Bitrate is persisted in kbps
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
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;
[self saveData];
}];
@@ -73,8 +88,8 @@
// Add a new persistent managed object if one doesn't exist
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
if (parent == nil) {
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:_managedObjectContext];
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:self->_managedObjectContext];
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
}
// Push changes from the temp host to the persistent one
@@ -98,8 +113,8 @@
// Add a new persistent managed object if one doesn't exist
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
if (parentApp == nil) {
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:_managedObjectContext];
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:self->_managedObjectContext];
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
}
[app propagateChangesToParent:parentApp withHost:parent];
@@ -113,20 +128,6 @@
}];
}
- (void) updateIconForExistingApp:(TemporaryApp*)app {
[_managedObjectContext performBlockAndWait:^{
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
if (parentApp == nil) {
// The app must exist to be updated
return;
}
parentApp.image = app.image;
[self saveData];
}];
}
- (TemporarySettings*) getSettings {
__block TemporarySettings *tempSettings;
@@ -155,7 +156,7 @@
[_managedObjectContext performBlockAndWait:^{
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
if (managedApp != nil) {
[_managedObjectContext deleteObject:managedApp];
[self->_managedObjectContext deleteObject:managedApp];
[self saveData];
}
}];
@@ -165,7 +166,7 @@
[_managedObjectContext performBlockAndWait:^{
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
if (managedHost != nil) {
[_managedObjectContext deleteObject:managedHost];
[self->_managedObjectContext deleteObject:managedHost];
[self saveData];
}
}];
@@ -173,7 +174,7 @@
- (void) saveData {
NSError* error;
if (![_managedObjectContext save:&error]) {
if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
Log(LOG_E, @"Unable to save hosts to database: %@", error);
}
@@ -209,7 +210,7 @@
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
for (App* app in apps) {
if ([app.id isEqualToString:tempApp.id] &&
[app.host.uuid isEqualToString:app.host.uuid]) {
[app.host.uuid isEqualToString:tempApp.host.uuid]) {
return app;
}
}
@@ -1,38 +0,0 @@
//
// Host+CoreDataProperties.h
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
// to keep your implementation up to date with your model.
//
#import "Host.h"
NS_ASSUME_NONNULL_BEGIN
@interface Host (CoreDataProperties)
@property (nullable, nonatomic, retain) NSString *address;
@property (nullable, nonatomic, retain) NSString *externalAddress;
@property (nullable, nonatomic, retain) NSString *localAddress;
@property (nullable, nonatomic, retain) NSString *mac;
@property (nullable, nonatomic, retain) NSString *name;
@property (nullable, nonatomic, retain) NSNumber *pairState;
@property (nullable, nonatomic, retain) NSString *uuid;
@property (nullable, nonatomic, retain) NSSet *appList;
@end
@interface Host (CoreDataGeneratedAccessors)
- (void)addAppListObject:(NSManagedObject *)value;
- (void)removeAppListObject:(NSManagedObject *)value;
- (void)addAppList:(NSSet *)values;
- (void)removeAppList:(NSSet *)values;
@end
NS_ASSUME_NONNULL_END
-17
View File
@@ -1,17 +0,0 @@
//
// Host.h
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Utils.h"
@interface Host : NSManagedObject
@end
#import "Host+CoreDataProperties.h"
-13
View File
@@ -1,13 +0,0 @@
//
// Host.m
// Moonlight
//
// Created by Diego Waxemberg on 7/10/15.
// Copyright © 2015 Limelight Stream. All rights reserved.
//
#import "Host.h"
@implementation Host
@end
-22
View File
@@ -1,22 +0,0 @@
//
// Settings.h
// Moonlight
//
// Created by Diego Waxemberg on 10/28/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
@interface Settings : NSManagedObject
@property (nonatomic, retain) NSNumber * bitrate;
@property (nonatomic, retain) NSNumber * framerate;
@property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) NSNumber * onscreenControls;
@property (nonatomic, retain) NSString * uniqueId;
@end
-21
View File
@@ -1,21 +0,0 @@
//
// Settings.m
// Moonlight
//
// Created by Diego Waxemberg on 10/28/14.
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import "Settings.h"
@implementation Settings
@dynamic bitrate;
@dynamic framerate;
@dynamic height;
@dynamic width;
@dynamic onscreenControls;
@dynamic uniqueId;
@end
+2 -3
View File
@@ -6,15 +6,14 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryHost.h"
#import "App.h"
#import "App+CoreDataClass.h"
@interface TemporaryApp : NSObject
@property (nullable, nonatomic, retain) NSString *id;
@property (nullable, nonatomic, retain) NSData *image;
@property (nullable, nonatomic, retain) NSString *name;
@property (nonatomic) BOOL hdrSupported;
@property (nullable, nonatomic, retain) TemporaryHost *host;
NS_ASSUME_NONNULL_BEGIN
+2 -2
View File
@@ -7,7 +7,6 @@
//
#import "TemporaryApp.h"
#import "App.h"
@implementation TemporaryApp
@@ -15,8 +14,8 @@
self = [self init];
self.id = app.id;
self.image = app.image;
self.name = app.name;
self.hdrSupported = app.hdrSupported;
self.host = tempHost;
return self;
@@ -25,6 +24,7 @@
- (void) propagateChangesToParent:(App*)parent withHost:(Host*)host {
parent.id = self.id;
parent.name = self.name;
parent.hdrSupported = self.hdrSupported;
parent.host = host;
}
+8 -6
View File
@@ -6,9 +6,8 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "Utils.h"
#import "Host.h"
#import "Host+CoreDataClass.h"
@interface TemporaryHost : NSObject
@@ -17,12 +16,15 @@
@property (nonatomic, nullable) NSString * activeAddress;
@property (nonatomic, nullable) 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 *mac;
@property (nonatomic) int serverCodecModeSupport;
NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, retain) NSString *address;
@property (nonatomic, retain) NSString *externalAddress;
@property (nonatomic, retain) NSString *localAddress;
@property (nonatomic, retain) NSString *mac;
@property (nonatomic, retain) NSString *name;
@property (nonatomic, retain) NSString *uuid;
@property (nonatomic, retain) NSMutableSet *appList;
+24 -7
View File
@@ -8,9 +8,7 @@
#import "DataManager.h"
#import "TemporaryHost.h"
#import "Host.h"
#import "TemporaryApp.h"
#import "App.h"
@implementation TemporaryHost
@@ -31,7 +29,11 @@
self.mac = host.mac;
self.name = host.name;
self.uuid = host.uuid;
self.pairState = [host.pairState intValue];
self.serverCodecModeSupport = host.serverCodecModeSupport;
self.serverCert = host.serverCert;
// Ensure we don't use a stale cached pair state if we haven't pinned the cert yet
self.pairState = host.serverCert ? [host.pairState intValue] : PairStateUnpaired;
NSMutableSet *appList = [[NSMutableSet alloc] init];
@@ -46,12 +48,27 @@
}
- (void) propagateChangesToParent:(Host*)parentHost {
parentHost.address = self.address;
parentHost.externalAddress = self.externalAddress;
parentHost.localAddress = self.localAddress;
parentHost.mac = self.mac;
// Avoid overwriting existing data with nil if
// we don't have everything populated in the temporary
// host.
if (self.address != nil) {
parentHost.address = self.address;
}
if (self.externalAddress != nil) {
parentHost.externalAddress = self.externalAddress;
}
if (self.localAddress != nil) {
parentHost.localAddress = self.localAddress;
}
if (self.mac != nil) {
parentHost.mac = self.mac;
}
if (self.serverCert != nil) {
parentHost.serverCert = self.serverCert;
}
parentHost.name = self.name;
parentHost.uuid = self.uuid;
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
}
+7 -2
View File
@@ -6,8 +6,7 @@
// Copyright © 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "Settings.h"
#import "Settings+CoreDataClass.h"
@interface TemporarySettings : NSObject
@@ -19,6 +18,12 @@
@property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) NSNumber * onscreenControls;
@property (nonatomic, retain) NSString * uniqueId;
@property (nonatomic) BOOL streamingRemotely;
@property (nonatomic) BOOL useHevc;
@property (nonatomic) BOOL multiController;
@property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
- (id) initFromSettings:(Settings*)settings;
+48
View File
@@ -7,6 +7,7 @@
//
#import "TemporarySettings.h"
#import "OnScreenControls.h"
@implementation TemporarySettings
@@ -15,12 +16,59 @@
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];
}
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;
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
case 0:
self.height = [NSNumber numberWithInteger:720];
self.width = [NSNumber numberWithInteger:1280];
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;
}
self.onscreenControls = (NSInteger)OnScreenControlsLevelOff;
#else
self.bitrate = settings.bitrate;
self.framerate = settings.framerate;
self.height = settings.height;
self.width = settings.width;
self.useHevc = settings.useHevc;
self.playAudioOnPC = settings.playAudioOnPC;
self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController;
self.onscreenControls = settings.onscreenControls;
#endif
self.uniqueId = settings.uniqueId;
self.streamingRemotely = settings.streamingRemotely;
return self;
}
-203
View File
@@ -1,203 +0,0 @@
Font data copyright Google 2012
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5 -6
View File
@@ -2,18 +2,17 @@
"images" : [
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "L2_60x60@1x.png"
"filename" : "L2_60x60@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "L2_120x120@2x.png"
"filename" : "L2_120x120@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "L2_180x180@3x.png"
"scale" : "3x"
}
],
"info" : {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

+5 -6
View File
@@ -2,18 +2,17 @@
"images" : [
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "R2_60x60@1x.png"
"filename" : "R2_60x60@1x.png",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "R2_120x120@2x.png"
"filename" : "R2_120x120@2x.png",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "R2_120x120@3x.png"
"scale" : "3x"
}
],
"info" : {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

+26
View File
@@ -0,0 +1,26 @@
//
// Controller.h
// Moonlight
//
// Created by Cameron Gutman on 2/11/19.
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
//
@import GameController;
@interface Controller : NSObject
@property (nullable, nonatomic, retain) GCController* gamepad;
@property (nonatomic) int playerIndex;
@property (nonatomic) int lastButtonFlags;
@property (nonatomic) int emulatingButtonFlags;
@property (nonatomic) unsigned char lastLeftTrigger;
@property (nonatomic) unsigned char lastRightTrigger;
@property (nonatomic) short lastLeftStickX;
@property (nonatomic) short lastLeftStickY;
@property (nonatomic) short lastRightStickX;
@property (nonatomic) short lastRightStickY;
@property (nonatomic) unsigned short lowFreqMotor;
@property (nonatomic) unsigned short highFreqMotor;
@end
+13
View File
@@ -0,0 +1,13 @@
//
// Controller.m
// Moonlight
//
// Created by Cameron Gutman on 2/11/19.
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
//
#include "Controller.h"
@implementation Controller
@end
-26
View File
@@ -1,26 +0,0 @@
//
// Controller.swift
// Moonlight
//
// Created by David Aghassi on 4/11/16.
// Copyright © 2016 Moonlight Stream. All rights reserved.
//
import Foundation
@objc
/**
Defines a controller layout
*/
class Controller: NSObject {
// Swift requires initial properties
var playerIndex: CInt = 0 // Controller number (e.g. 1, 2 ,3 etc)
var lastButtonFlags: CInt = 0
var emulatingButtonFlags: CInt = 0
var lastLeftTrigger: CChar = 0 // Last left trigger pressed
var lastRightTrigger: CChar = 0 // Last right trigger pressed
var lastLeftStickX: CShort = 0 // Last X direction the left joystick went
var lastLeftStickY: CShort = 0 // Last Y direction the left joystick went
var lastRightStickX: CShort = 0 // Last X direction the right joystick went
var lastRightStickY: CShort = 0 // Last Y direction the right joystick went
}
+18 -6
View File
@@ -6,19 +6,28 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
// Swift
#import "Moonlight-Swift.h"
@class Controller;
#if !TARGET_OS_IPHONE
#import "Gamepad.h"
#endif
#import "StreamConfiguration.h"
#import "Controller.h"
@class OnScreenControls;
@interface ControllerSupport : NSObject
-(id) init;
-(id) initWithConfig:(StreamConfiguration*)streamConfig;
#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;
@@ -32,7 +41,10 @@
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
-(void) updateFinished:(Controller*)controller;
-(Controller*) getOscController;
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
@property (nonatomic, strong) id connectObserver;
@property (nonatomic, strong) id disconnectObserver;
+233 -30
View File
@@ -7,18 +7,24 @@
//
#import "ControllerSupport.h"
#import "Controller.h"
#import "OnScreenControls.h"
#if !TARGET_OS_IPHONE
#import "Gamepad.h"
#import "Control.h"
#endif
#import "DataManager.h"
#include "Limelight.h"
// Swift
#import "Moonlight-Swift.h"
@class Controller;
@import GameController;
@import AudioToolbox;
@implementation ControllerSupport {
NSLock *_controllerStreamLock;
NSMutableDictionary *_controllers;
NSTimer *_rumbleTimer;
OnScreenControls *_osc;
@@ -26,16 +32,50 @@
// and player 0
Controller *_player0osc;
char _controllerNumbers;
#define EMULATING_SELECT 0x1
#define EMULATING_SPECIAL 0x2
bool _oscEnabled;
char _controllerNumbers;
bool _multiController;
}
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
#define UPDATE_BUTTON_FLAG(controller, x, y) \
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
-(void) rumbleController: (Controller*)controller
{
// Only vibrate if the amplitude is large enough
if (controller.lowFreqMotor > 0x5000 || controller.highFreqMotor > 0x5000) {
// If the gamepad is nil (on-screen controls) or it's attached to the device,
// then vibrate the device itself
if (controller.gamepad == nil || [controller.gamepad isAttachedToDevice]) {
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
}
}
}
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
{
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
if (controller == nil && controllerNumber == 0 && _oscEnabled) {
// No physical controller, but we have on-screen controls
controller = _player0osc;
}
if (controller == nil) {
// No connected controller for this player
return;
}
// Update the motor levels for the rumble timer to grab next iteration
controller.lowFreqMotor = lowFreqMotor;
controller.highFreqMotor = highFreqMotor;
// Rumble now to ensure short vibrations aren't missed
[self rumbleController:controller];
}
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
{
@synchronized(controller) {
@@ -112,17 +152,16 @@
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
{
@synchronized(controller) {
int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
controller.lastButtonFlags = flags;
// This must be called before handleSpecialCombosPressed
// because we clear the original button flags there
int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
[self handleSpecialCombosReleased:controller releasedButtons:releasedButtons];
[self handleSpecialCombosPressed:controller pressedButtons:pressedButtons];
}
}
@@ -147,7 +186,8 @@
[_controllerStreamLock lock];
@synchronized(controller) {
// Player 1 is always present for OSC
LiSendMultiControllerEvent(controller.playerIndex, _controllerNumbers | 1, controller.lastButtonFlags, controller.lastLeftTrigger, controller.lastRightTrigger, controller.lastLeftStickX, controller.lastLeftStickY, controller.lastRightStickX, controller.lastRightStickY);
LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0,
(_multiController ? _controllerNumbers : 1) | (_oscEnabled ? 1 : 0), controller.lastButtonFlags, controller.lastLeftTrigger, controller.lastRightTrigger, controller.lastLeftStickX, controller.lastLeftStickY, controller.lastRightStickX, controller.lastRightStickY);
}
[_controllerStreamLock unlock];
}
@@ -170,20 +210,24 @@
{
if (controller != NULL) {
controller.controllerPausedHandler = ^(GCController *controller) {
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
[self setButtonFlag:limeController flags:PLAY_FLAG];
[self updateFinished:limeController];
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
// Pause for 100 ms
usleep(100 * 1000);
[self clearButtonFlag:limeController flags:PLAY_FLAG];
[self updateFinished:limeController];
// Get off the main thread
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self setButtonFlag:limeController flags:PLAY_FLAG];
[self updateFinished:limeController];
// Pause for 100 ms
usleep(100 * 1000);
[self clearButtonFlag:limeController flags:PLAY_FLAG];
[self updateFinished:limeController];
});
};
if (controller.extendedGamepad != NULL) {
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
short leftStickX, leftStickY;
short rightStickX, rightStickY;
char leftTrigger, rightTrigger;
@@ -201,6 +245,16 @@
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
// Yay, iOS 12.1 now supports analog stick buttons
if (@available(iOS 12.1, tvOS 12.1, *)) {
if (gamepad.leftThumbstickButton != nil) {
UPDATE_BUTTON_FLAG(limeController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed);
}
if (gamepad.rightThumbstickButton != nil) {
UPDATE_BUTTON_FLAG(limeController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed);
}
}
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
@@ -218,7 +272,7 @@
}
else if (controller.gamepad != NULL) {
controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
@@ -257,6 +311,12 @@
if (controller != NULL) {
if (controller.extendedGamepad != NULL) {
level = OnScreenControlsLevelAutoGCExtendedGamepad;
if (@available(iOS 12.1, tvOS 12.1, *)) {
if (controller.extendedGamepad.leftThumbstickButton != nil &&
controller.extendedGamepad.rightThumbstickButton != nil) {
level = OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons;
}
}
break;
}
else if (controller.gamepad != NULL) {
@@ -266,7 +326,9 @@
}
}
#if TARGET_OS_IPHONE
[_osc setLevel:level];
#endif
}
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
@@ -283,6 +345,7 @@
controller.playerIndex = i;
Controller* limeController;
if (i == 0) {
// Player 0 shares a controller object with the on-screen controls
limeController = _player0osc;
@@ -290,6 +353,8 @@
limeController = [[Controller alloc] init];
limeController.playerIndex = i;
}
limeController.gamepad = controller;
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
@@ -299,31 +364,153 @@
}
}
#if TARGET_OS_IPHONE
-(Controller*) getOscController {
return _player0osc;
}
#else
-(NSMutableDictionary*) getControllers {
return _controllers;
}
-(id) init
-(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;
}
+(int) getGamepadCount {
int count = 0;
for (GCController* controller in [GCController controllers]) {
if ([ControllerSupport isSupportedGamepad:controller]) {
count++;
}
}
return count;
}
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig {
int mask = 0;
if (streamConfig.multiController) {
int i = 0;
for (GCController* controller in [GCController controllers]) {
if ([ControllerSupport isSupportedGamepad:controller]) {
mask |= 1 << i++;
}
}
}
else {
// Some games don't deal with having controller reconnected
// properly so always report controller 1 if not in MC mode
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) {
mask |= 0x1;
}
#endif
return mask;
}
-(void) rumbleTimer
{
for (int i = 0; i < 4; i++) {
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
if (controller == nil && i == 0 && _oscEnabled) {
// No physical controller, but we have on-screen controls
controller = _player0osc;
}
if (controller == nil) {
// No connected controller for this player
continue;
}
[self rumbleController:controller];
}
}
-(id) initWithConfig:(StreamConfiguration*)streamConfig
{
self = [super init];
_controllerStreamLock = [[NSLock alloc] init];
_controllers = [[NSMutableDictionary alloc] init];
_controllerNumbers = 0;
_multiController = streamConfig.multiController;
_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
selector:@selector(rumbleTimer)
userInfo:nil
repeats:YES];
Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
Log(LOG_I, @"Multi-controller: %d", _multiController);
Log(LOG_I, @"Number of controllers connected: %ld", (long)[[GCController controllers] count]);
for (GCController* controller in [GCController controllers]) {
[self assignController:controller];
[self registerControllerCallbacks:controller];
[self updateAutoOnScreenControlMode];
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) {
Log(LOG_I, @"Controller connected!");
GCController* controller = note.object;
if (![ControllerSupport isSupportedGamepad:controller]) {
// Ignore micro gamepads and motion controllers
return;
}
[self assignController:controller];
// Register callbacks on the new controller
@@ -336,26 +523,42 @@
Log(LOG_I, @"Controller disconnected!");
GCController* controller = note.object;
if (![ControllerSupport isSupportedGamepad:controller]) {
// Ignore micro gamepads and motion controllers
return;
}
[self unregisterControllerCallbacks:controller];
[_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
_controllerNumbers &= ~(1 << controller.playerIndex);
self->_controllerNumbers &= ~(1 << controller.playerIndex);
Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex);
// Unset the GCController on this object (in case it is the OSC, which will persist)
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
limeController.gamepad = nil;
// Inform the server of the updated active gamepads before removing this controller
[self updateFinished:limeController];
[self->_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
}];
return self;
}
-(void) cleanup
{
[_rumbleTimer invalidate];
_rumbleTimer = nil;
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
[_controllers removeAllObjects];
_controllerNumbers = 0;
for (GCController* controller in [GCController controllers]) {
[self unregisterControllerCallbacks:controller];
if ([ControllerSupport isSupportedGamepad:controller]) {
[self unregisterControllerCallbacks:controller];
}
}
}
+21
View File
@@ -0,0 +1,21 @@
//
// KeyboardSupport.h
// Moonlight
//
// Created by Diego Waxemberg on 8/25/18.
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface KeyboardSupport : NSObject
struct KeyEvent {
u_short keycode;
u_short modifierKeycode;
u_char modifier;
};
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
@end
+191
View File
@@ -0,0 +1,191 @@
//
// KeyboardSupport.m
// Moonlight
//
// Created by Diego Waxemberg on 8/25/18.
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
//
#import "KeyboardSupport.h"
#include <Limelight.h>
@implementation KeyboardSupport
+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
struct KeyEvent event;
event.keycode = 0;
event.modifier = 0;
event.modifierKeycode = 0;
switch (modifierFlags) {
case UIKeyModifierAlphaShift:
case UIKeyModifierShift:
[KeyboardSupport addShiftModifier:&event];
break;
case UIKeyModifierControl:
[KeyboardSupport addControlModifier:&event];
break;
case UIKeyModifierAlternate:
[KeyboardSupport addAltModifier:&event];
break;
case UIKeyModifierCommand:
case UIKeyModifierNumericPad:
break;
}
if (inputChar >= 0x30 && inputChar <= 0x39) {
// Numbers 0-9
event.keycode = inputChar;
} else if (inputChar >= 0x41 && inputChar <= 0x5A) {
// Capital letters
event.keycode = inputChar;
[KeyboardSupport addShiftModifier:&event];
} else if (inputChar >= 0x61 && inputChar <= 0x7A) {
// Lower case letters
event.keycode = inputChar - (0x61 - 0x41);
} switch (inputChar) {
case ' ': // Spacebar
event.keycode = 0x20;
break;
case '-': // Hyphen '-'
event.keycode = 0xBD;
break;
case '/': // Forward slash '/'
event.keycode = 0xBF;
break;
case ':': // Colon ':'
event.keycode = 0xBA;
[KeyboardSupport addShiftModifier:&event];
break;
case ';': // Semi-colon ';'
event.keycode = 0xBA;
break;
case '(': // Open parenthesis '('
event.keycode = 0x39; // '9'
[KeyboardSupport addShiftModifier:&event];
break;
case ')': // Close parenthesis ')'
event.keycode = 0x30; // '0'
[KeyboardSupport addShiftModifier:&event];
break;
case '$': // Dollar sign '$'
event.keycode = 0x34; // '4'
[KeyboardSupport addShiftModifier:&event];
break;
case '&': // Ampresand '&'
event.keycode = 0x37; // '7'
[KeyboardSupport addShiftModifier:&event];
break;
case '@': // At-sign '@'
event.keycode = 0x32; // '2'
[KeyboardSupport addShiftModifier:&event];
break;
case '"':
event.keycode = 0xDE;
[KeyboardSupport addShiftModifier:&event];
break;
case '\'':
event.keycode = 0xDE;
break;
case '!':
event.keycode = 0x31; // '1'
[KeyboardSupport addShiftModifier:&event];
break;
case '?':
event.keycode = 0xBF; // '/'
[KeyboardSupport addShiftModifier:&event];
break;
case ',':
event.keycode = 0xBC;
break;
case '<':
event.keycode = 0xBC;
[KeyboardSupport addShiftModifier:&event];
break;
case '.':
event.keycode = 0xBE;
break;
case '>':
event.keycode = 0xBE;
[KeyboardSupport addShiftModifier:&event];
break;
case '[':
event.keycode = 0xDB;
break;
case ']':
event.keycode = 0xDD;
break;
case '{':
event.keycode = 0xDB;
[KeyboardSupport addShiftModifier:&event];
break;
case '}':
event.keycode = 0xDD;
[KeyboardSupport addShiftModifier:&event];
break;
case '#':
event.keycode = 0x33; // '3'
[KeyboardSupport addShiftModifier:&event];
break;
case '%':
event.keycode = 0x35; // '5'
[KeyboardSupport addShiftModifier:&event];
break;
case '^':
event.keycode = 0x36; // '6'
[KeyboardSupport addShiftModifier:&event];
break;
case '*':
event.keycode = 0x38; // '8'
[KeyboardSupport addShiftModifier:&event];
break;
case '+':
event.keycode = 0xBB;
[KeyboardSupport addShiftModifier:&event];
break;
case '=':
event.keycode = 0xBB;
break;
case '_':
event.keycode = 0xBD;
[KeyboardSupport addShiftModifier:&event];
break;
case '\\':
event.keycode = 0xDC;
break;
case '|':
event.keycode = 0xDC;
[KeyboardSupport addShiftModifier:&event];
break;
case '~':
event.keycode = 0xC0;
[KeyboardSupport addShiftModifier:&event];
break;
case '`':
event.keycode = 0xC0;
break;
case '\t':
event.keycode = 0x09;
break;
default:
break;
}
return event;
}
+ (void) addShiftModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_SHIFT;
event->modifierKeycode = 0x10;
}
+ (void) addControlModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_CTRL;
event->modifierKeycode = 0x11;
}
+ (void) addAltModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_ALT;
event->modifierKeycode = 0x12;
}
@end
+3
View File
@@ -22,12 +22,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
// Internal levels selected by ControllerSupport
OnScreenControlsLevelAutoGCGamepad,
OnScreenControlsLevelAutoGCExtendedGamepad,
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
@end
+108 -70
View File
@@ -8,12 +8,9 @@
#import "OnScreenControls.h"
#import "ControllerSupport.h"
//#import "Controller.h"
#import "Controller.h"
#include "Limelight.h"
#import "Moonlight-Swift.h"
@class Controller;
#define UPDATE_BUTTON(x, y) (buttonFlags = \
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
@@ -202,15 +199,27 @@ static float L3_Y;
[self hideSticks];
[self drawL3R3];
break;
case OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons:
// This variant of GCExtendedGamepad has L3 and R3 but
// is still missing Select
[self setupExtendedGamepadControls];
[self hideButtons];
[self hideBumpers];
[self hideTriggers];
[self hideL3R3];
[self drawStartSelect];
[self hideSticks];
break;
case OnScreenControlsLevelSimple:
[self setupSimpleControls];
[self drawTriggers];
[self drawStartSelect];
[self drawL3R3];
[self hideButtons];
[self hideTriggers];
[self hideL3R3];
[self hideBumpers];
[self hideSticks];
[self drawStartSelect];
[self drawButtons];
break;
case OnScreenControlsLevelFull:
[self setupComplexControls];
@@ -238,8 +247,22 @@ static float L3_Y;
// Start with the default complex layout
[self setupComplexControls];
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
R3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
if (_iPad) {
L3_X = _controlArea.size.width * .15 + _controlArea.origin.x;
R3_X = _controlArea.size.width * .85 + _controlArea.origin.x;
}
else {
L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
}
}
// For GCGamepad controls we move triggers, start, and select
@@ -248,41 +271,21 @@ static float L3_Y;
// Start with the default complex layout
[self setupComplexControls];
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
R2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
if (_iPad) {
START_X = _controlArea.size.width * .75 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
SELECT_X = _controlArea.size.width * .25 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
// The analog sticks are kept closer to the sides on iPad
LS_CENTER_X = _controlArea.size.width * .18 + _controlArea.origin.x;
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
RS_CENTER_X = _controlArea.size.width * .82 + _controlArea.origin.x;
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
} else {
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
LS_CENTER_X = _controlArea.size.width * .25 + _controlArea.origin.x;
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
RS_CENTER_X = _controlArea.size.width * .75 + _controlArea.origin.x;
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
LS_CENTER_X = _controlArea.size.width * .15 + _controlArea.origin.x;
RS_CENTER_X = _controlArea.size.width * .85 + _controlArea.origin.x;
}
}
@@ -291,10 +294,7 @@ static float L3_Y;
// Start with the default complex layout
[self setupComplexControls];
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
@@ -302,6 +302,20 @@ static float L3_Y;
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
if (_iPad) {
// Lower the D-pad and buttons on iPad
D_PAD_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
BUTTON_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
// Move Start and Select closer to sides
SELECT_X = _controlArea.size.width * .2 + _controlArea.origin.x;
START_X = _controlArea.size.width * .8 + _controlArea.origin.x;
}
else {
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
}
}
- (void) setupComplexControls
@@ -315,9 +329,9 @@ static float L3_Y;
{
// The analog sticks are kept closer to the sides on iPad
LS_CENTER_X = _controlArea.size.width * .22 + _controlArea.origin.x;
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
LS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
RS_CENTER_X = _controlArea.size.width * .77 + _controlArea.origin.x;
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
RS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
}
else
{
@@ -332,18 +346,24 @@ static float L3_Y;
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
L1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
L2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
R1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
R2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
L3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
R3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
if (_iPad) {
// Move L/R buttons closer to the side on iPad
L1_X = _controlArea.size.width * .05 + _controlArea.origin.x;
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
R1_X = _controlArea.size.width * .95 + _controlArea.origin.x;
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
}
else {
L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
}
}
- (void) drawButtons {
@@ -836,26 +856,31 @@ static float L3_Y;
}
- (BOOL) isInDeadZone:(UITouch*) touch {
switch (_level) {
case OnScreenControlsLevelFull:
return [self isDpadDeadZone:touch]
|| [self isAbxyDeadZone:touch]
|| [self isTriggerDeadZone:touch]
|| [self isBumperDeadZone:touch]
|| [self isStartSelectDeadZone:touch];
case OnScreenControlsLevelSimple:
return [self isTriggerDeadZone:touch]
|| [self isStartSelectDeadZone:touch]
|| [self isL3R3DeadZone:touch];
case OnScreenControlsLevelAutoGCExtendedGamepad:
return [self isL3R3DeadZone:touch]
|| [self isStartSelectDeadZone:touch];
case OnScreenControlsLevelAutoGCGamepad:
return [self isTriggerDeadZone:touch]
|| [self isStartSelectDeadZone:touch];
default:
return false;
// Dynamically evaluate deadzones based on the controls
// on screen at the time
if (_leftButton.superlayer != nil && [self isDpadDeadZone:touch]) {
return true;
}
else if (_aButton.superlayer != nil && [self isAbxyDeadZone:touch]) {
return true;
}
else if (_l2Button.superlayer != nil && [self isTriggerDeadZone:touch]) {
return true;
}
else if (_l1Button.superlayer != nil && [self isBumperDeadZone:touch]) {
return true;
}
else if (_startButton.superlayer != nil && [self isStartSelectDeadZone:touch]) {
return true;
}
else if (_l3Button.superlayer != nil && [self isL3R3DeadZone:touch]) {
return true;
}
else if (_leftStickBackground.superlayer != nil && [self isStickDeadZone:touch]) {
return true;
}
return false;
}
- (BOOL) isDpadDeadZone:(UITouch*) touch {
@@ -926,6 +951,19 @@ static float L3_Y;
endY:_view.frame.origin.y + _view.frame.size.height];
}
- (BOOL) isStickDeadZone:(UITouch*) touch {
return [self isDeadZone:touch
startX:_leftStickBackground.frame.origin.x - 15
startY:_leftStickBackground.frame.origin.y - 15
endX:_leftStickBackground.frame.origin.x + _leftStickBackground.frame.size.width + 15
endY:_view.frame.origin.y + _view.frame.size.height]
|| [self isDeadZone:touch
startX:_rightStickBackground.frame.origin.x - 15
startY:_rightStickBackground.frame.origin.y - 15
endX:_rightStickBackground.frame.origin.x + _rightStickBackground.frame.size.width + 15
endY:_view.frame.origin.y + _view.frame.size.height];
}
- (BOOL) isDeadZone:(UITouch*) touch startX:(float)deadZoneStartX startY:(float)deadZoneStartY endX:(float)deadZoneEndX endY:(float)deadZoneEndY {
deadZoneStartX -= DEAD_ZONE_PADDING;
deadZoneStartY -= DEAD_ZONE_PADDING;
+3 -2
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <UIKit/UIKit.h>
#import "ControllerSupport.h"
@protocol EdgeDetectionDelegate <NSObject>
@@ -15,7 +14,9 @@
@end
@interface StreamView : UIView
@interface StreamView : OSView <UITextFieldDelegate>
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
- (void) setMouseDeltaFactors:(float)x y:(float)y;
+160 -25
View File
@@ -11,18 +11,22 @@
#import "OnScreenControls.h"
#import "DataManager.h"
#import "ControllerSupport.h"
#import "KeyboardSupport.h"
@implementation StreamView {
CGPoint touchLocation, originalLocation;
BOOL touchMoved;
OnScreenControls* onScreenControls;
BOOL isInputingText;
BOOL isDragging;
NSTimer* dragTimer;
float xDeltaFactor;
float yDeltaFactor;
float screenFactor;
NSDictionary<NSString *, NSNumber *> *dictCodes;
}
- (void) setMouseDeltaFactors:(float)x y:(float)y {
@@ -44,11 +48,12 @@
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
[onScreenControls setLevel:level];
}
[self becomeFirstResponder];
}
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
// Movements of greater than 20 pixels are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 20;
// Movements of greater than 10 pixels are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10;
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
@@ -76,8 +81,6 @@
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if (![onScreenControls handleTouchMovedEvent:touches]) {
[dragTimer invalidate];
dragTimer = nil;
if ([[event allTouches] count] == 1) {
UITouch *touch = [[event allTouches] anyObject];
CGPoint currentLocation = [touch locationInView:self];
@@ -128,33 +131,54 @@
if (![onScreenControls handleTouchUpEvent:touches]) {
[dragTimer invalidate];
dragTimer = nil;
if (!touchMoved) {
if ([[event allTouches] count] == 2) {
Log(LOG_D, @"Sending right mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
} else {
if (!isDragging){
Log(LOG_D, @"Sending left mouse button press");
if (isDragging) {
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
} else if (!touchMoved) {
if ([[event allTouches] count] == 3) {
if (isInputingText) {
Log(LOG_D, @"Closing the keyboard");
[_keyInputField resignFirstResponder];
isInputingText = false;
} else {
Log(LOG_D, @"Opening the keyboard");
// Prepare the textbox used to capture keyboard events.
_keyInputField.delegate = self;
_keyInputField.text = @"0";
[_keyInputField becomeFirstResponder];
[_keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Undo causes issues for our state management, so turn it off
[_keyInputField.undoManager disableUndoRegistration];
isInputingText = true;
}
} else if ([[event allTouches] count] == 2) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
Log(LOG_D, @"Sending right mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
}
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
} else if ([[event allTouches] count] == 1) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (!self->isDragging){
Log(LOG_D, @"Sending left mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
}
self->isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
}
}
else if (isDragging) {
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
// We we're moving from 2+ touches to 1. Synchronize the current position
// of the active finger so we don't jump unexpectedly on the next touchesMoved
@@ -175,5 +199,116 @@
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
}
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
// This method is called when the "Return" key is pressed.
LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
usleep(50 * 1000);
LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
return NO;
}
- (void)onKeyboardPressed:(UITextField *)textField {
NSString* inputText = textField.text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// If the text became empty, we know the user pressed the backspace key.
if ([inputText isEqual:@""]) {
LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
usleep(50 * 1000);
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
} else {
// Character 0 will be our known sentinel value
for (int i = 1; i < [inputText length]; i++) {
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
if (event.keycode == 0) {
// If we don't know the code, don't send anything.
Log(LOG_W, @"Unknown key code: [%c]", [inputText characterAtIndex:i]);
continue;
}
[self sendLowLevelEvent:event];
}
}
});
// Reset text field back to known state
textField.text = @"0";
// Move the insertion point back to the end of the text box
UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
[textField setSelectedTextRange:textRange];
}
- (void)specialCharPressed:(UIKeyCommand *)cmd {
struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
[self sendLowLevelEvent:event];
}
- (void)keyPressed:(UIKeyCommand *)cmd {
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
[self sendLowLevelEvent:event];
}
- (void)sendLowLevelEvent:(struct KeyEvent)event {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
// When we want to send a modified key (like uppercase letters) we need to send the
// modifier ("shift") seperately from the key itself.
if (event.modifier != 0) {
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
}
LiSendKeyboardEvent(event.keycode, KEY_ACTION_DOWN, event.modifier);
usleep(50 * 1000);
LiSendKeyboardEvent(event.keycode, KEY_ACTION_UP, event.modifier);
if (event.modifier != 0) {
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
}
});
}
- (BOOL)canBecomeFirstResponder {
return YES;
}
- (NSArray<UIKeyCommand *> *)keyCommands
{
NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
NSMutableArray<UIKeyCommand *> * commands = [NSMutableArray<UIKeyCommand *> array];
dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
[charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
options:NSStringEnumerationByComposedCharacterSequences
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
}];
for (NSString *c in [dictCodes keyEnumerator]) {
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:0
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierShift
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierShift | UIKeyModifierControl
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierControl
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
action:@selector(specialCharPressed:)]];
[commands addObject:[UIKeyCommand keyCommandWithInput:c
modifierFlags:UIKeyModifierAlternate
action:@selector(specialCharPressed:)]];
}
return commands;
}
@end
+17 -11
View File
@@ -2,6 +2,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>CADisableMinimumFrameDuration</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
@@ -17,11 +19,24 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.4.0</string>
<string>2.6.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>2</string>
<string>1</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>LSApplicationCategoryType</key>
<string>public.app-category.games</string>
<key>LSRequiresIPhoneOS</key>
@@ -31,15 +46,6 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>UIAppFonts</key>
<array>
<string>Roboto-Black.ttf</string>
<string>Roboto-Bold.ttf</string>
<string>Roboto-Light.ttf</string>
<string>Roboto-Medium.ttf</string>
<string>Roboto-Regular.ttf</string>
<string>Roboto-Thin.ttf</string>
</array>
<key>UILaunchStoryboardName</key>
<string>Launch Screen</string>
<key>UIMainStoryboardFile</key>
+11 -4
View File
@@ -5,14 +5,21 @@
//
#import <Availability.h>
#include <TargetConditionals.h>
#ifndef __IPHONE_3_0
#warning "This project uses features only available in iOS SDK 3.0 and later."
#endif
#ifdef __OBJC__
#if TARGET_OS_IPHONE
#import <UIKit/UIKit.h>
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Logger.h"
#endif
#elif TARGET_OS_MAC
#import <AppKit/AppKit.h>
#endif
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Logger.h"
#include "OSPortabilityDefs.h"
#endif
@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Moonlight v1.0-2.xcdatamodel</string>
<string>Moonlight v1.3.xcdatamodel</string>
</dict>
</plist>
@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9057" systemVersion="15B42" minimumToolsVersion="Automatic">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="17D102" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<entity name="App" representedClassName="App" syncable="YES">
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
@@ -12,21 +12,22 @@
<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" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" 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">
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" 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="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="streamingRemotely" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" 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="163"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="135"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="150"/>
</elements>
</model>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17F77" 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="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" 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="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="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="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="165"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
</elements>
</model>
@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" 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="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="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="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="165"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
</elements>
</model>
@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" 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="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="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="195"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
</elements>
</model>
+1 -1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryApp.h"
#import "HttpManager.h"
#import "TemporaryHost.h"
@@ -22,5 +21,6 @@
- (id) initWithCallback:(id<AppAssetCallback>)callback;
- (void) retrieveAssetsFromHost:(TemporaryHost*)host;
- (void) stopRetrieving;
+ (NSString*) boxArtPathForApp:(TemporaryApp*)app;
@end
+17 -1
View File
@@ -19,6 +19,22 @@
static const int MAX_REQUEST_COUNT = 4;
+ (NSString*) boxArtPathForApp:(TemporaryApp*)app {
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
NSString *filePath = [paths objectAtIndex:0];
// Keep app assets separate by host UUID
filePath = [filePath stringByAppendingPathComponent:app.host.uuid];
// Use the app ID as the file name
filePath = [filePath stringByAppendingPathComponent:app.id];
// Add a png extension
filePath = [filePath stringByAppendingPathExtension:@"png"];
return filePath;
}
- (id) initWithCallback:(id<AppAssetCallback>)callback {
self = [super init];
_callback = callback;
@@ -30,7 +46,7 @@ static const int MAX_REQUEST_COUNT = 4;
- (void) retrieveAssetsFromHost:(TemporaryHost*)host {
for (TemporaryApp* app in host.appList) {
if (app.image == nil) {
if (![[NSFileManager defaultManager] fileExistsAtPath:[AppAssetManager boxArtPathForApp:app]]) {
AppAssetRetriever* retriever = [[AppAssetRetriever alloc] init];
retriever.app = app;
retriever.host = host;
-1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpResponse.h"
@interface AppAssetResponse : NSObject <Response>
+6 -3
View File
@@ -16,9 +16,12 @@
self.statusMessage = @"App asset has no status message";
self.statusCode = -1;
}
- (UIImage*) getImage {
UIImage* appImage = [[UIImage alloc] initWithData:self.data];
- (OSImage*) getImage {
#if TARGET_OS_IPHONE
OSImage* appImage = [[OSImage alloc] initWithData:self.data];
#else
OSImage* appImage = nil;
#endif
return appImage;
}
-1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryHost.h"
#import "TemporaryApp.h"
#import "AppAssetManager.h"
+13 -7
View File
@@ -18,18 +18,24 @@ static const double RETRY_DELAY = 2; // seconds
static const int MAX_ATTEMPTS = 5;
- (void) main {
UIImage* appImage = nil;
int attempts = 0;
while (![self isCancelled] && appImage == nil && attempts++ < MAX_ATTEMPTS) {
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] deviceName:deviceName cert:[CryptoManager readCertFromFile]];
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] serverCert:_host.serverCert];
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
appImage = [UIImage imageWithData:appAssetResp.data];
self.app.image = UIImagePNGRepresentation(appImage);
#endif
if (![self isCancelled] && appImage == nil) {
if (![self isCancelled]) {
[NSThread sleepForTimeInterval:RETRY_DELAY];
}
}
+9 -2
View File
@@ -19,6 +19,7 @@
static const char* TAG_APP = "App";
static const char* TAG_APP_TITLE = "AppTitle";
static const char* TAG_APP_ID = "ID";
static const char* TAG_HDR_SUPPORTED = "IsHdrSupported";
- (void)populateWithData:(NSData *)xml {
self.data = xml;
@@ -66,14 +67,13 @@ static const char* TAG_APP_ID = "ID";
xmlNodePtr appInfoNode = node->xmlChildrenNode;
NSString* appName = @"";
NSString* appId = nil;
NSString* hdrSupported = @"0";
while (appInfoNode != NULL) {
if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_TITLE)) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
if (nodeVal != NULL) {
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
} else {
appName = @"";
}
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_ID)) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
@@ -81,6 +81,12 @@ static const char* TAG_APP_ID = "ID";
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
}
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_HDR_SUPPORTED)) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
if (nodeVal != NULL) {
hdrSupported = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
}
}
appInfoNode = appInfoNode->next;
}
@@ -88,6 +94,7 @@ static const char* TAG_APP_ID = "ID";
TemporaryApp* app = [[TemporaryApp alloc] init];
app.name = appName;
app.id = appId;
app.hdrSupported = [hdrSupported intValue] != 0;
[_appList addObject:app];
}
}
+21
View File
@@ -0,0 +1,21 @@
//
// ConnectionHelper.h
// Moonlight macOS
//
// Created by Felix Kratz on 22.03.18.
// Copyright © 2018 Felix Kratz. All rights reserved.
//
#import "AppListResponse.h"
#ifndef ConnectionHelper_h
#define ConnectionHelper_h
@interface ConnectionHelper : NSObject
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) serverCert uniqueID:(NSString*) uniqueId;
@end
#endif /* ConnectionHelper_h */
+40
View File
@@ -0,0 +1,40 @@
//
// ConnectionHelper.m
// Moonlight macOS
//
// Created by Felix Kratz on 22.03.18.
// Copyright © 2018 Felix Kratz. All rights reserved.
//
#import "ConnectionHelper.h"
#import "ServerInfoResponse.h"
#import "HttpManager.h"
#import "PairManager.h"
#import "DiscoveryManager.h"
@implementation ConnectionHelper
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) cert uniqueID:(NSString*) uniqueId {
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
// Try up to 5 times to get the app list
AppListResponse* appListResp;
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) {
Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
// Wait for one second then retry
[NSThread sleepForTimeInterval:1];
}
else {
Log(LOG_I, @"App list successfully retreived - took %d tries", i);
return appListResp;
}
}
return nil;
}
@end
-1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "MDNSManager.h"
#import "TemporaryHost.h"
+45 -27
View File
@@ -41,16 +41,16 @@
_opQueue = [[NSOperationQueue alloc] init];
_mdnsMan = [[MDNSManager alloc] initWithCallback:self];
[CryptoManager generateKeyPairUsingSSl];
[CryptoManager generateKeyPairUsingSSL];
_uniqueId = [IdManager getUniqueId];
_cert = [CryptoManager readCertFromFile];
return self;
}
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId serverCert:nil];
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
TemporaryHost* host = nil;
if ([serverInfoResponse isStatusOk]) {
@@ -59,7 +59,7 @@
host.online = YES;
[serverInfoResponse populateHost:host];
if (![self addHostToDiscovery:host]) {
callback(nil, @"Host already added");
callback(nil, @"Host information updated");
} else {
callback(host, nil);
}
@@ -70,6 +70,10 @@
}
- (void) startDiscovery {
if (shouldDiscover) {
return;
}
Log(LOG_I, @"Starting discovery");
shouldDiscover = YES;
[_mdnsMan searchForHosts];
@@ -79,6 +83,10 @@
}
- (void) stopDiscovery {
if (!shouldDiscover) {
return;
}
Log(LOG_I, @"Stopping discovery");
shouldDiscover = NO;
[_mdnsMan stopSearching];
@@ -87,10 +95,18 @@
- (void) stopDiscoveryBlocking {
Log(LOG_I, @"Stopping discovery and waiting for workers to stop");
shouldDiscover = NO;
[_mdnsMan stopSearching];
[_opQueue cancelAllOperations];
if (shouldDiscover) {
shouldDiscover = NO;
[_mdnsMan stopSearching];
[_opQueue cancelAllOperations];
}
// Ensure we always wait, just in case discovery
// was stopped already but in an async manner that
// left operations in progress.
[_opQueue waitUntilAllOperationsAreFinished];
Log(LOG_I, @"All discovery workers stopped");
}
@@ -101,6 +117,12 @@
TemporaryHost *existingHost = [self getHostInDiscovery:host.uuid];
if (existingHost != nil) {
// NB: Our logic here depends on the fact that we never propagate
// the entire TemporaryHost to existingHost. In particular, when mDNS
// discovers a PC and we poll it, we will do so over HTTP which will
// not have accurate pair state. The fields explicitly copied below
// are accurate though.
// Update address of existing host
if (host.address != nil) {
existingHost.address = host.address;
@@ -112,6 +134,7 @@
existingHost.externalAddress = host.externalAddress;
}
existingHost.activeAddress = host.activeAddress;
existingHost.online = host.online;
return NO;
}
else {
@@ -142,28 +165,23 @@
[_hostQueue removeObject:host];
}
// Override from MDNSCallback
- (void)updateHosts:(NSArray *)hosts {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// Discover the hosts before adding to eliminate duplicates
for (TemporaryHost* host in hosts) {
Log(LOG_I, @"Found host through MDNS: %@:", host.name);
// Since this is on a background thread, we do not need to use the opQueue
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
[worker discoverHost];
if ([self addHostToDiscovery:host]) {
Log(LOG_I, @"Adding host to discovery: %@", host.name);
[_callback updateAllHosts:_hostQueue];
} else {
Log(LOG_I, @"Not adding host to discovery: %@", host.name);
}
}
});
// Override from MDNSCallback - called in a worker thread
- (void)updateHost:(TemporaryHost*)host {
// Discover the hosts before adding to eliminate duplicates
Log(LOG_D, @"Found host through MDNS: %@:", host.name);
// Since this is on a background thread, we do not need to use the opQueue
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
[worker discoverHost];
if ([self addHostToDiscovery:host]) {
Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
[self->_callback updateAllHosts:self->_hostQueue];
} else {
Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
}
}
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
for (int i = 0; i < _hostQueue.count; i++) {
TemporaryHost* discoveredHost = [_hostQueue objectAtIndex:i];
for (TemporaryHost* discoveredHost in _hostQueue) {
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
return discoveredHost;
}
@@ -172,7 +190,7 @@
}
- (NSOperation*) createWorkerForHost:(TemporaryHost*)host {
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId cert:_cert];
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId];
return worker;
}
+1 -2
View File
@@ -6,12 +6,11 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryHost.h"
@interface DiscoveryWorker : NSOperation
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert;
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId;
- (void) discoverHost;
- (TemporaryHost*) getHost;
+11 -18
View File
@@ -16,16 +16,14 @@
@implementation DiscoveryWorker {
TemporaryHost* _host;
NSString* _uniqueId;
NSData* _cert;
}
static const float POLL_RATE = 2.0f; // Poll every 2 seconds
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert {
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId {
self = [super init];
_host = host;
_uniqueId = uniqueId;
_cert = cert;
return self;
}
@@ -48,12 +46,12 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
if (_host.localAddress != nil) {
[array addObject:_host.localAddress];
}
if (_host.externalAddress != nil) {
[array addObject:_host.externalAddress];
}
if (_host.address != nil) {
[array addObject:_host.address];
}
if (_host.externalAddress != nil) {
[array addObject:_host.externalAddress];
}
// Remove duplicate addresses from the list.
// This is done using an array rather than a set
@@ -88,8 +86,8 @@ 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 3 tries to respond before declaring it offline
for (int i = 0; i < 3; i++) {
// Give the PC 2 tries to respond before declaring it offline
for (int i = 0; i < 2; i++) {
for (NSString *address in addresses) {
if (self.cancelled) {
// Get out without updating the status because
@@ -98,7 +96,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
return;
}
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address];
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
receivedResponse = [self checkResponse:serverInfoResp];
if (receivedResponse) {
[serverInfoResp populateHost:_host];
@@ -112,13 +110,9 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
}
if (receivedResponse) {
Log(LOG_I, @"Received serverinfo response on try %d", i);
Log(LOG_D, @"Received serverinfo response on try %d", i);
break;
}
else {
// Wait for one second then retry
[NSThread sleepForTimeInterval:1];
}
}
_host.online = receivedResponse;
@@ -127,14 +121,13 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
}
}
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address {
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
uniqueId:_uniqueId
deviceName:deviceName
cert:_cert];
serverCert:cert];
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
withUrlRequest:[hMan newServerInfoRequest]
withUrlRequest:[hMan newServerInfoRequest:true]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
return response;
}
+8 -6
View File
@@ -6,24 +6,26 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpResponse.h"
#import "HttpRequest.h"
#import "StreamConfiguration.h"
@interface HttpManager : NSObject <NSURLSessionDelegate>
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert;
- (NSURLRequest*) newPairRequest:(NSData*)salt;
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
- (void) setServerCert:(NSData*) serverCert;
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
- (NSURLRequest*) newUnpairRequest;
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge;
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp;
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret;
- (NSURLRequest*) newPairChallenge;
- (NSURLRequest*) newAppListRequest;
- (NSURLRequest*) newServerInfoRequest;
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest;
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid;
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId;
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newQuitAppRequest;
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
- (void) executeRequestSynchronously:(HttpRequest*)request;
+138 -52
View File
@@ -14,19 +14,23 @@
#include <libxml2/libxml/xmlreader.h>
#include <string.h>
#define SHORT_TIMEOUT_SEC 2
#define NORMAL_TIMEOUT_SEC 5
#define LONG_TIMEOUT_SEC 60
#define EXTRA_LONG_TIMEOUT_SEC 180
@implementation HttpManager {
NSURLSession* _urlSession;
NSString* _baseHTTPURL;
NSString* _baseHTTPSURL;
NSString* _host;
NSString* _uniqueId;
NSString* _deviceName;
NSData* _cert;
NSData* _serverCert;
NSMutableData* _respData;
NSData* _requestResp;
dispatch_semaphore_t _requestLock;
BOOL _errorOccurred;
NSError* _error;
}
static const NSString* HTTP_PORT = @"47989";
@@ -39,49 +43,66 @@ static const NSString* HTTPS_PORT = @"47984";
return [xmlString dataUsingEncoding:NSUTF8StringEncoding];
}
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert {
- (void) setServerCert:(NSData*) serverCert {
_serverCert = serverCert;
}
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert {
self = [super init];
_host = host;
_uniqueId = uniqueId;
// Use the same UID for all Moonlight clients to allow them
// quit games started on another Moonlight client.
_uniqueId = @"0123456789ABCDEF";
_deviceName = deviceName;
_cert = cert;
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", host, HTTP_PORT];
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", host, HTTPS_PORT];
_serverCert = serverCert;
_requestLock = dispatch_semaphore_create(0);
_respData = [[NSMutableData alloc] init];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
// If this is an IPv6 literal, we must properly enclose it in brackets
NSString* urlSafeHost;
if ([host containsString:@":"]) {
urlSafeHost = [NSString stringWithFormat:@"[%@]", host];
} else {
urlSafeHost = host;
}
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", urlSafeHost, HTTP_PORT];
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", urlSafeHost, HTTPS_PORT];
return self;
}
- (void) executeRequestSynchronously:(HttpRequest*)request {
Log(LOG_D, @"Making Request: %@", request);
[_respData setLength:0];
_error = nil;
Log(LOG_D, @"Making Request: %@", request);
[[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
if (error != NULL) {
Log(LOG_D, @"Connection error: %@", error);
_errorOccurred = true;
self->_error = error;
}
else {
Log(LOG_D, @"Received response: %@", response);
if (data != NULL) {
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
[_respData appendData:data];
if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) {
_requestResp = [HttpManager fixXmlVersion:_respData];
[self->_respData appendData:data];
if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
} else {
_requestResp = _respData;
self->_requestResp = self->_respData;
}
}
}
dispatch_semaphore_signal(_requestLock);
dispatch_semaphore_signal(self->_requestLock);
}] resume];
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
if (!_errorOccurred && request.response) {
if (!_error && request.response) {
[request.response populateWithData:_requestResp];
// If the fallback error code was detected, issue the fallback request
@@ -93,92 +114,127 @@ static const NSString* HTTPS_PORT = @"47984";
[self executeRequestSynchronously:request];
}
}
_errorOccurred = false;
else if (_error && [_error code] == NSURLErrorServerCertificateUntrusted) {
// We must have a pinned cert for HTTPS. If we fail, it must be due to
// a non-matching cert, not because we had no cert at all.
assert(_serverCert != nil);
if (request.fallbackRequest) {
// This will fall back to HTTP on serverinfo queries to allow us to pair again
// and get the server cert updated.
Log(LOG_D, @"Attempting fallback request after certificate trust failure");
request.request = request.fallbackRequest;
request.fallbackError = 0;
request.fallbackRequest = NULL;
[self executeRequestSynchronously:request];
}
}
}
- (NSURLRequest*) createRequestFromString:(NSString*) urlString enableTimeout:(BOOL)normalTimeout {
- (NSURLRequest*) createRequestFromString:(NSString*) urlString timeout:(int)timeout {
NSURL* url = [[NSURL alloc] initWithString:urlString];
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
if (normalTimeout) {
// Timeout the request after 7 seconds
[request setTimeoutInterval:7];
}
else {
// Timeout the request after 60 seconds
[request setTimeoutInterval:60];
}
[request setTimeoutInterval:timeout];
return request;
}
- (NSURLRequest*) newPairRequest:(NSData*)salt {
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=getservercert&salt=%@&clientcert=%@",
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:_cert]];
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:clientCert]];
// This call blocks while waiting for the user to input the PIN on the PC
return [self createRequestFromString:urlString enableTimeout:FALSE];
return [self createRequestFromString:urlString timeout:EXTRA_LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newUnpairRequest {
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString enableTimeout:TRUE];
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPURL, _uniqueId];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
return [self createRequestFromString:urlString enableTimeout:TRUE];
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&serverchallengeresp=%@",
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
return [self createRequestFromString:urlString enableTimeout:TRUE];
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPSURL, _uniqueId, _deviceName, clientPairSecret];
return [self createRequestFromString:urlString enableTimeout:TRUE];
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPURL, _uniqueId, _deviceName, clientPairSecret];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest*) newPairChallenge {
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
return [self createRequestFromString:urlString enableTimeout:TRUE];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest *)newAppListRequest {
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString enableTimeout:TRUE];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest *)newServerInfoRequest {
- (NSURLRequest *)newServerInfoRequest:(bool)fastFail {
if (_serverCert == nil) {
// Use HTTP if the cert is not pinned yet
return [self newHttpServerInfoRequest:fastFail];
}
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString enableTimeout:TRUE];
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
}
- (NSURLRequest *)newHttpServerInfoRequest:(bool)fastFail {
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
}
- (NSURLRequest *)newHttpServerInfoRequest {
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
return [self createRequestFromString:urlString enableTimeout:TRUE];
return [self newHttpServerInfoRequest:false];
}
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid {
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=1&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, appId, width, height, refreshRate, rikey, rikeyid];
- (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;
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,
config.appID,
config.width, config.height, fps,
config.optimizeGameSettings ? 1 : 0,
[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,
config.gamepadMask, config.gamepadMask];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is launching
return [self createRequestFromString:urlString enableTimeout:FALSE];
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId {
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, riKey, riKeyId];
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
_baseHTTPSURL, _uniqueId,
[Utils bytesToHex:config.riKey], config.riKeyId,
(config.audioChannelMask << 16) | config.audioChannelCount];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is resuming
return [self createRequestFromString:urlString enableTimeout:FALSE];
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newQuitAppRequest {
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString enableTimeout:FALSE];
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
return [self createRequestFromString:urlString enableTimeout:FALSE];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSString*) bytesToHex:(NSData*)data {
@@ -229,6 +285,36 @@ static const NSString* HTTPS_PORT = @"47984";
// Allow untrusted server certificates
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
{
if (SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust) != 1) {
Log(LOG_E, @"Server certificate count mismatch");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
return;
}
SecCertificateRef actualCert = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, 0);
if (actualCert == nil) {
Log(LOG_E, @"Server certificate parsing error");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
return;
}
CFDataRef actualCertData = SecCertificateCopyData(actualCert);
if (actualCertData == nil) {
Log(LOG_E, @"Server certificate data parsing error");
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
return;
}
if (!CFEqual(actualCertData, (__bridge CFDataRef)_serverCert)) {
Log(LOG_E, @"Server certificate mismatch");
CFRelease(actualCertData);
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
return;
}
CFRelease(actualCertData);
// Allow TLS handshake to proceed
completionHandler(NSURLSessionAuthChallengeUseCredential,
[NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]);
}
-1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpResponse.h"
@interface HttpRequest : NSObject
-2
View File
@@ -6,8 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
static NSString* TAG_STATUS_CODE = @"status_code";
static NSString* TAG_STATUS_MESSAGE = @"status_message";
+2
View File
@@ -89,6 +89,8 @@
}
xmlFreeDoc(docPtr);
Log(LOG_D, @"Parsed XML data: %@", _elements);
}
@end
+2 -2
View File
@@ -6,11 +6,11 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryHost.h"
@protocol MDNSCallback <NSObject>
- (void) updateHosts:(NSArray*)hosts;
- (void) updateHost:(TemporaryHost*)host;
@end
+66 -32
View File
@@ -9,11 +9,15 @@
#import "MDNSManager.h"
#import "TemporaryHost.h"
#include <arpa/inet.h>
#include <Limelight.h>
@implementation MDNSManager {
NSNetServiceBrowser* mDNSBrowser;
NSMutableArray* domains;
NSMutableArray* services;
BOOL scanActive;
BOOL timerPending;
}
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
@@ -23,43 +27,65 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
self.callback = callback;
scanActive = FALSE;
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
[mDNSBrowser setDelegate:self];
domains = [[NSMutableArray alloc] init];
services = [[NSMutableArray alloc] init];
return self;
}
- (void) searchForHosts {
if (scanActive) {
return;
}
Log(LOG_I, @"Starting mDNS discovery");
scanActive = TRUE;
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
if (!timerPending) {
timerPending = TRUE;
// Just invoke the timer callback to save a little code
[self startSearchTimerCallback:nil];
}
}
- (void) stopSearching {
if (!scanActive) {
return;
}
Log(LOG_I, @"Stopping mDNS discovery");
scanActive = FALSE;
[mDNSBrowser stop];
}
- (NSArray*) getFoundHosts {
NSMutableArray* hosts = [[NSMutableArray alloc] init];
for (NSNetService* service in services) {
if (service.hostName != nil) {
TemporaryHost* host = [[TemporaryHost alloc] init];
host.activeAddress = host.localAddress = service.hostName;
host.name = service.hostName;
[hosts addObject:host];
}
}
return hosts;
}
- (void)netServiceDidResolveAddress:(NSNetService *)service {
Log(LOG_I, @"Resolved address: %@ -> %@", service, service.hostName);
[self.callback updateHosts:[self getFoundHosts]];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
Log(LOG_D, @"Resolved address: %@ -> %@", service, service.hostName);
TemporaryHost* host = [[TemporaryHost alloc] init];
// Since we discovered this host over 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];
}
else {
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
}
host.activeAddress = host.localAddress = service.hostName;
host.name = service.hostName;
[self.callback updateHost:host];
});
}
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
@@ -74,12 +100,14 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
Log(LOG_I, @"Found service: %@", aNetService);
[aNetService setDelegate:self];
[aNetService resolveWithTimeout:5];
Log(LOG_D, @"Found service: %@", aNetService);
[services removeObject:aNetService];
[services addObject:aNetService];
if (![services containsObject:aNetService]) {
Log(LOG_I, @"Found new host: %@", aNetService.name);
[aNetService setDelegate:self];
[aNetService resolveWithTimeout:5];
[services addObject:aNetService];
}
}
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
@@ -90,23 +118,29 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
Log(LOG_W, @"Did not perform search: \n%@", [errorDict description]);
// Schedule a retry in 2 seconds
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(retrySearchTimerCallback:)
userInfo:nil
repeats:NO];
// We'll schedule a retry in startSearchTimerCallback
}
- (void)retrySearchTimerCallback:(NSTimer *)timer {
- (void)startSearchTimerCallback:(NSTimer *)timer {
// Check if we've been stopped since this was queued
if (!scanActive) {
timerPending = FALSE;
return;
}
Log(LOG_I, @"Retrying mDNS search");
Log(LOG_D, @"Restarting mDNS search");
[mDNSBrowser stop];
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
// Search again in 5 seconds. We need to do this because
// we want more aggressive querying than Bonjour will normally
// do for when we're at the hosts screen. This also covers scenarios
// where discovery didn't work, like if WiFi was disabled.
[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
selector:@selector(startSearchTimerCallback:)
userInfo:nil
repeats:NO];
}
- (void)retryResolveTimerCallback:(NSTimer *)timer {
@@ -124,4 +158,4 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
}
}
@end
@end
+2 -3
View File
@@ -6,20 +6,19 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "HttpManager.h"
@protocol PairCallback <NSObject>
- (void) showPIN:(NSString*)PIN;
- (void) pairSuccessful;
- (void) pairSuccessful:(NSData*)serverCert;
- (void) pairFailed:(NSString*)message;
- (void) alreadyPaired;
@end
@interface PairManager : NSOperation
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback;
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback;
- (NSString*) generatePIN;
- (NSData*) saltPIN:(NSString*)PIN;
- (void) initiatePair:(int)serverMajorVersion;
+27 -24
View File
@@ -17,28 +17,28 @@
@implementation PairManager {
HttpManager* _httpManager;
NSData* _cert;
NSData* _clientCert;
id<PairCallback> _callback;
}
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback {
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback {
self = [super init];
_httpManager = httpManager;
_cert = cert;
_clientCert = clientCert;
_callback = callback;
return self;
}
- (void) main {
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest]
[_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_AVAILABLE"]) {
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
[_callback pairFailed:@"You must stop streaming before attempting to pair."];
} else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
@@ -53,6 +53,7 @@
}
}
// All codepaths must call pairFailed or pairSuccessful exactly once before returning!
- (void) initiatePair:(int)serverMajorVersion {
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
@@ -62,8 +63,9 @@
[_callback showPIN:PIN];
HttpResponse* pairResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt]]];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
if (![self verifyResponseStatus:pairResp]) {
[_callback pairFailed:@"Pairing stage #1 failed"];
return;
}
NSInteger pairedStatus;
@@ -78,6 +80,10 @@
return;
}
// Pin the cert for TLS usage on this host
NSData* derCertBytes = [CryptoManager pemToDer:[Utils hexToBytes:plainCert]];
[_httpManager setServerCert:derCertBytes];
CryptoManager* cryptoMan = [[CryptoManager alloc] init];
NSData* aesKey;
@@ -97,10 +103,9 @@
HttpResponse* challengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
if (![self verifyResponseStatus:challengeResp]) {
return;
}
if (![challengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
if (![self verifyResponseStatus:challengeResp] ||
![challengeResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[_callback pairFailed:@"Pairing stage #2 failed"];
return;
@@ -113,7 +118,7 @@
NSData* serverChallenge = [decServerChallengeResp subdataWithRange:NSMakeRange(hashLength, 16)];
NSData* clientSecret = [Utils randomBytes:16];
NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_cert]] with:clientSecret];
NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_clientCert]] with:clientSecret];
NSData* challengeRespHash;
if (serverMajorVersion >= 7) {
challengeRespHash = [cryptoMan SHA256HashData: challengeRespHashInput];
@@ -125,10 +130,9 @@
HttpResponse* secretResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
if (![self verifyResponseStatus:secretResp]) {
return;
}
if (![secretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
if (![self verifyResponseStatus:secretResp] ||
![secretResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[_callback pairFailed:@"Pairing stage #3 failed"];
return;
@@ -161,10 +165,9 @@
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]) {
return;
}
if (![clientSecretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
if (![self verifyResponseStatus:clientSecretResp] ||
![clientSecretResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[_callback pairFailed:@"Pairing stage #4 failed"];
return;
@@ -172,15 +175,15 @@
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
if (![self verifyResponseStatus:clientPairChallengeResp]) {
return;
}
if (![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
if (![self verifyResponseStatus:clientPairChallengeResp] ||
![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] ||
pairedStatus != 1) {
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
[_callback pairFailed:@"Pairing stage #5 failed"];
return;
}
[_callback pairSuccessful];
[_callback pairSuccessful: derCertBytes];
}
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
+13 -2
View File
@@ -19,14 +19,20 @@
- (void) populateHost:(TemporaryHost*)host {
host.name = [[self getStringTag:TAG_HOSTNAME] trim];
host.externalAddress = [[self getStringTag:TAG_EXTERNAL_IP] trim];
host.localAddress = [[self getStringTag:TAG_LOCAL_IP] trim];
host.uuid = [[self getStringTag:TAG_UNIQUE_ID] trim];
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
// Modern GFE versions don't actually give us a WAN address anymore
// so we leave the one that we populated from mDNS discovery via STUN.
NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim];
if (wanAddr) {
host.externalAddress = wanAddr;
}
NSString *state = [[self getStringTag:TAG_STATE] trim];
if ([state hasSuffix:@"_SERVER_AVAILABLE"]) {
if (![state hasSuffix:@"_SERVER_BUSY"]) {
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
// has the semantics that its name would indicate. To contain the effects of this change as much
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
@@ -39,6 +45,11 @@
} else {
host.pairState = PairStateUnknown;
}
NSString *serverCodecModeString = [self getStringTag:@"ServerCodecModeSupport"];
if (serverCodecModeString != nil) {
host.serverCodecModeSupport = [[serverCodecModeString trim] intValue];
}
}
@end
-1
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "TemporaryHost.h"
@interface WakeOnLanManager : NSObject
+7 -1
View File
@@ -33,7 +33,7 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
+ (void) wakeHost:(TemporaryHost*)host {
NSData* wolPayload = [WakeOnLanManager createPayload:host];
for (int i = 0; i < 3; i++) {
for (int i = 0; i < 4; i++) {
const char* address;
struct addrinfo hints, *res, *curr;
@@ -44,6 +44,8 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
address = [host.externalAddress UTF8String];
} else if (i == 2 && host.address != nil) {
address = [host.address UTF8String];
} else if (i == 3) {
address = "255.255.255.255";
} else {
// Requested address wasn't present
continue;
@@ -63,6 +65,7 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
// may be different address families.
for (curr = res; curr != NULL; curr = curr->ai_next) {
int wolSocket;
int val;
wolSocket = socket(curr->ai_family, SOCK_DGRAM, IPPROTO_UDP);
if (wolSocket < 0) {
@@ -70,6 +73,9 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
continue;
}
val = 1;
setsockopt(wolSocket, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
struct sockaddr_storage addr;
memset(&addr, 0, sizeof(addr));
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
+24
View File
@@ -0,0 +1,24 @@
//
// OSPortabilityDefs.h
// Moonlight
//
// Created by Cameron Gutman on 3/26/18.
// Copyright © 2018 Moonlight Stream. All rights reserved.
//
#ifndef OSPortabilityDefs_h
#define OSPortabilityDefs_h
#if TARGET_OS_IPHONE
#define OSImage UIImage
#define OSColor UIColor
#define OSView UIView
#define OSApplication UIApplication
#else
#define OSImage NSImage
#define OSColor NSColor
#define OSView NSView
#define OSApplication NSApplication
#endif
#endif /* OSPortabilityDefs_h */
+1 -3
View File
@@ -6,7 +6,6 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "VideoDecoderRenderer.h"
#import "StreamConfiguration.h"
@@ -18,8 +17,7 @@
- (void) stageComplete:(const char*)stageName;
- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
- (void) launchFailed:(NSString*)message;
- (void) displayMessage:(const char*)message;
- (void) displayTransientMessage:(const char*)message;
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
@end
+199 -238
View File
@@ -13,7 +13,7 @@
#import <VideoToolbox/VideoToolbox.h>
#include "Limelight.h"
#include "opus.h"
#include "opus_multistream.h"
@implementation Connection {
SERVER_INFORMATION _serverInfo;
@@ -21,29 +21,31 @@
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
DECODER_RENDERER_CALLBACKS _drCallbacks;
AUDIO_RENDERER_CALLBACKS _arCallbacks;
char _hostString[256];
char _appVersionString[32];
char _gfeVersionString[32];
}
static NSLock* initLock;
static OpusDecoder *opusDecoder;
static OpusMSDecoder* opusDecoder;
static id<ConnectionCallbacks> _callbacks;
#define PCM_BUFFER_SIZE 1024
#define OUTPUT_BUS 0
struct AUDIO_BUFFER_QUEUE_ENTRY {
struct AUDIO_BUFFER_QUEUE_ENTRY *next;
int length;
int offset;
char data[0];
};
#define MAX_CHANNEL_COUNT 2
#define FRAME_SIZE 240
#define MAX_QUEUE_ENTRIES 10
#define CIRCULAR_BUFFER_SIZE 32
static short decodedPcmBuffer[512];
static NSLock *audioLock;
static struct AUDIO_BUFFER_QUEUE_ENTRY *audioBufferQueue;
static int audioBufferQueueLength;
static AudioComponentInstance audioUnit;
static int audioBufferWriteIndex;
static int audioBufferReadIndex;
static int activeChannelCount;
static short audioCircularBuffer[CIRCULAR_BUFFER_SIZE][FRAME_SIZE * MAX_CHANNEL_COUNT];
#define AUDIO_QUEUE_BUFFERS 4
static AudioQueueRef audioQueue;
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
static VideoDecoderRenderer* renderer;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
@@ -55,62 +57,69 @@ int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void*
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
{
int offset = 0;
int ret;
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
if (data == NULL) {
// A frame was lost due to OOM condition
return DR_NEED_IDR;
}
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
memcpy(&data[offset], entry->data, entry->length);
offset += entry->length;
// 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];
if (ret != DR_OK) {
free(data);
return ret;
}
}
else {
memcpy(&data[offset], entry->data, entry->length);
offset += entry->length;
}
entry = entry->next;
}
// This function will take our buffer
return [renderer submitDecodeBuffer:data length:decodeUnit->fullLength];
// This function will take our picture data buffer
return [renderer submitDecodeBuffer:data length:offset bufferType:BUFFER_TYPE_PICDATA];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
{
int err;
// We only support stereo for now
// Clear the circular buffer
audioBufferWriteIndex = audioBufferReadIndex = 0;
// We only support stereo for now.
// TODO: Ensure AudioToolbox's channel mapping matches Opus's
// and correct if neccessary.
assert(audioConfiguration == AUDIO_CONFIGURATION_STEREO);
opusDecoder = opus_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
&err);
audioLock = [[NSLock alloc] init];
activeChannelCount = opusConfig->channelCount;
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 setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
[audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
[audioSession setActive: YES error: &audioSessionError];
#endif
OSStatus status;
AudioComponentDescription audioDesc;
audioDesc.componentType = kAudioUnitType_Output;
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
audioDesc.componentFlags = 0;
audioDesc.componentFlagsMask = 0;
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
status = AudioComponentInstanceNew(AudioComponentFindNext(NULL, &audioDesc), &audioUnit);
if (status) {
Log(LOG_E, @"Unable to instantiate new AudioComponent: %d", (int32_t)status);
return status;
}
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = opusConfig->sampleRate;
audioFormat.mBitsPerChannel = 16;
@@ -121,42 +130,26 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame;
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
audioFormat.mReserved = 0;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_StreamFormat,
kAudioUnitScope_Input,
OUTPUT_BUS,
&audioFormat,
sizeof(audioFormat));
if (status) {
Log(LOG_E, @"Unable to set audio unit to input: %d", (int32_t)status);
status = AudioQueueNewOutput(&audioFormat, FillOutputBuffer, nil, nil, nil, 0, &audioQueue);
if (status != noErr) {
Log(LOG_E, @"Error allocating output queue: %d\n", status);
return status;
}
AURenderCallbackStruct callbackStruct = {0};
callbackStruct.inputProc = playbackCallback;
callbackStruct.inputProcRefCon = NULL;
status = AudioUnitSetProperty(audioUnit,
kAudioUnitProperty_SetRenderCallback,
kAudioUnitScope_Input,
OUTPUT_BUS,
&callbackStruct,
sizeof(callbackStruct));
if (status) {
Log(LOG_E, @"Unable to set audio unit callback: %d", (int32_t)status);
return status;
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * FRAME_SIZE, &audioBuffers[i]);
if (status != noErr) {
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
return status;
}
FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
}
status = AudioUnitInitialize(audioUnit);
if (status) {
Log(LOG_E, @"Unable to initialize audioUnit: %d", (int32_t)status);
return status;
}
status = AudioOutputUnitStart(audioUnit);
if (status) {
Log(LOG_E, @"Unable to start audioUnit: %d", (int32_t)status);
status = AudioQueueStart(audioQueue, nil);
if (status != noErr) {
Log(LOG_E, @"Error starting queue: %d\n", status);
return status;
}
@@ -166,77 +159,44 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
void ArCleanup(void)
{
if (opusDecoder != NULL) {
opus_decoder_destroy(opusDecoder);
opus_multistream_decoder_destroy(opusDecoder);
opusDecoder = NULL;
}
OSStatus status = AudioOutputUnitStop(audioUnit);
if (status) {
Log(LOG_E, @"Unable to stop audioUnit: %d", (int32_t)status);
}
// Stop before disposing to avoid massive delay inside
// AudioQueueDispose() (iOS bug?)
AudioQueueStop(audioQueue, true);
status = AudioUnitUninitialize(audioUnit);
if (status) {
Log(LOG_E, @"Unable to uninitialize audioUnit: %d", (int32_t)status);
}
// Also frees buffers
AudioQueueDispose(audioQueue, true);
#if TARGET_OS_IPHONE
// Audio session is now inactive
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setActive: YES error: nil];
// This is safe because we're guaranteed that nobody
// is touching this list now
struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
while (audioBufferQueue != NULL) {
entry = audioBufferQueue;
audioBufferQueue = entry->next;
audioBufferQueueLength--;
free(entry);
}
#endif
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int decodedLength = opus_decode(opusDecoder, (unsigned char*)sampleData, sampleLength, decodedPcmBuffer, PCM_BUFFER_SIZE / 2, 0);
if (decodedLength > 0) {
// Return of opus_decode is samples per channel
decodedLength *= 4;
int decodeLen;
// 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) {
return;
}
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
audioCircularBuffer[audioBufferWriteIndex], FRAME_SIZE, 0);
if (decodeLen > 0) {
// Use a full memory barrier to ensure the circular buffer is written before incrementing the index
__sync_synchronize();
struct AUDIO_BUFFER_QUEUE_ENTRY *newEntry = malloc(sizeof(*newEntry) + decodedLength);
if (newEntry != NULL) {
newEntry->next = NULL;
newEntry->length = decodedLength;
newEntry->offset = 0;
memcpy(newEntry->data, decodedPcmBuffer, decodedLength);
[audioLock lock];
if (audioBufferQueueLength > MAX_QUEUE_ENTRIES) {
Log(LOG_W, @"Audio player too slow. Dropping all decoded samples!");
// Clear all values from the buffer queue
struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
while (audioBufferQueue != NULL) {
entry = audioBufferQueue;
audioBufferQueue = entry->next;
audioBufferQueueLength--;
free(entry);
}
}
if (audioBufferQueue == NULL) {
audioBufferQueue = newEntry;
}
else {
struct AUDIO_BUFFER_QUEUE_ENTRY *lastEntry = audioBufferQueue;
while (lastEntry->next != NULL) {
lastEntry = lastEntry->next;
}
lastEntry->next = newEntry;
}
audioBufferQueueLength++;
[audioLock unlock];
}
// 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;
}
}
@@ -265,16 +225,6 @@ void ClConnectionTerminated(long errorCode)
[_callbacks connectionTerminated: errorCode];
}
void ClDisplayMessage(const char* message)
{
[_callbacks displayMessage: message];
}
void ClDisplayTransientMessage(const char* message)
{
[_callbacks displayTransientMessage: message];
}
void ClLogMessage(const char* format, ...)
{
va_list va;
@@ -283,8 +233,19 @@ void ClLogMessage(const char* format, ...)
va_end(va);
}
void ClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
{
[_callbacks rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
}
-(void) terminate
{
// Interrupt any action blocking LiStartConnection(). This is
// thread-safe and done outside initLock on purpose, since we
// won't be able to acquire it if LiStartConnection is in
// progress.
LiInterruptConnection();
// We dispatch this async to get out because this can be invoked
// on a thread inside common and we don't want to deadlock. It also avoids
// blocking on the caller's thread waiting to acquire initLock.
@@ -298,149 +259,149 @@ void ClLogMessage(const char* format, ...)
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks
{
self = [super init];
// Use a lock to ensure that only one thread is initializing
// or deinitializing a connection at a time.
if (initLock == nil) {
initLock = [[NSLock alloc] init];
}
LiInitializeServerInformation(&_serverInfo);
_serverInfo.address = [config.host cStringUsingEncoding:NSUTF8StringEncoding];
_serverInfo.serverInfoAppVersion = [config.appVersion cStringUsingEncoding:NSUTF8StringEncoding];
strncpy(_hostString,
[config.host cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_hostString));
strncpy(_appVersionString,
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_appVersionString));
if (config.gfeVersion != nil) {
_serverInfo.serverInfoGfeVersion = [config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding];
strncpy(_gfeVersionString,
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_gfeVersionString));
}
LiInitializeServerInformation(&_serverInfo);
_serverInfo.address = _hostString;
_serverInfo.serverInfoAppVersion = _appVersionString;
if (config.gfeVersion != nil) {
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
}
renderer = myRenderer;
_callbacks = callbacks;
LiInitializeStreamConfiguration(&_streamConfig);
_streamConfig.width = config.width;
_streamConfig.height = config.height;
_streamConfig.fps = config.frameRate;
_streamConfig.bitrate = config.bitRate;
_streamConfig.enableHdr = config.enableHdr;
// On iOS 11, we can use HEVC if the server supports encoding it
// and this device has hardware decode for it (A9 and later)
if (@available(iOS 11.0, *)) {
_streamConfig.supportsHevc = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
// 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();
}
// FIXME: We should use 1024 when streaming remotely
_streamConfig.packetSize = 1292;
// HDR implies HEVC allowed
if (config.enableHdr) {
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
// to freeze after a few minutes with HEVC prior to iOS 11.3.
// As a result, we will only use HEVC on iOS 11.3 or later.
if (@available(iOS 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);
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
memset(_streamConfig.remoteInputAesIv, 0, 16);
int riKeyId = htonl(config.riKeyId);
memcpy(_streamConfig.remoteInputAesIv, &riKeyId, sizeof(riKeyId));
LiInitializeVideoCallbacks(&_drCallbacks);
_drCallbacks.setup = DrDecoderSetup;
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
// RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
// It doesnt work on macOS either, tested with Network Link Conditioner.
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
CAPABILITY_DIRECT_SUBMIT;
LiInitializeAudioCallbacks(&_arCallbacks);
_arCallbacks.init = ArInit;
_arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting;
_clCallbacks.stageComplete = ClStageComplete;
_clCallbacks.stageFailed = ClStageFailed;
_clCallbacks.connectionStarted = ClConnectionStarted;
_clCallbacks.connectionTerminated = ClConnectionTerminated;
_clCallbacks.displayMessage = ClDisplayMessage;
_clCallbacks.displayTransientMessage = ClDisplayTransientMessage;
_clCallbacks.logMessage = ClLogMessage;
_clCallbacks.rumble = ClRumble;
return self;
}
static OSStatus playbackCallback(void *inRefCon,
AudioUnitRenderActionFlags *ioActionFlags,
const AudioTimeStamp *inTimeStamp,
UInt32 inBusNumber,
UInt32 inNumberFrames,
AudioBufferList *ioData) {
// Notes: ioData contains buffers (may be more than one!)
// Fill them up as much as you can. Remember to set the size value in each buffer to match how
// much data is in the buffer.
static void FillOutputBuffer(void *aqData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
inBuffer->mAudioDataByteSize = activeChannelCount * FRAME_SIZE * sizeof(short);
bool ranOutOfData = false;
for (int i = 0; i < ioData->mNumberBuffers; i++) {
ioData->mBuffers[i].mNumberChannels = 2;
assert(inBuffer->mAudioDataByteSize == inBuffer->mAudioDataBytesCapacity);
// If the indexes aren't equal, we have a sample
if (audioBufferWriteIndex != audioBufferReadIndex) {
// Copy data to the audio buffer
memcpy(inBuffer->mAudioData,
audioCircularBuffer[audioBufferReadIndex],
inBuffer->mAudioDataByteSize);
if (ranOutOfData) {
ioData->mBuffers[i].mDataByteSize = 0;
continue;
}
// Use a full memory barrier to ensure the circular buffer is read before incrementing the index
__sync_synchronize();
if (ioData->mBuffers[i].mDataByteSize != 0) {
int thisBufferOffset = 0;
FillBufferAgain:
// Make sure there's data to write
if (ioData->mBuffers[i].mDataByteSize - thisBufferOffset == 0) {
continue;
}
struct AUDIO_BUFFER_QUEUE_ENTRY *audioEntry = NULL;
[audioLock lock];
if (audioBufferQueue != NULL) {
// Dequeue this entry temporarily
audioEntry = audioBufferQueue;
audioBufferQueue = audioBufferQueue->next;
audioBufferQueueLength--;
}
[audioLock unlock];
if (audioEntry == NULL) {
// No data left
ranOutOfData = true;
ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
continue;
}
// Figure out how much data we can write
int min = MIN(ioData->mBuffers[i].mDataByteSize - thisBufferOffset, audioEntry->length);
// Copy data to the audio buffer
memcpy(&ioData->mBuffers[i].mData[thisBufferOffset], &audioEntry->data[audioEntry->offset], min);
thisBufferOffset += min;
if (min < audioEntry->length) {
// This entry still has unused data
audioEntry->length -= min;
audioEntry->offset += min;
// Requeue the entry
[audioLock lock];
audioEntry->next = audioBufferQueue;
audioBufferQueue = audioEntry;
audioBufferQueueLength++;
[audioLock unlock];
}
else {
// This entry is fully depleted so free it
free(audioEntry);
// Try to grab another sample to fill this buffer with
goto FillBufferAgain;
}
ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
}
// 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;
}
else {
// No data, so play silence
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
}
return noErr;
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
-(void) main
{
[initLock lock];
+11 -2
View File
@@ -6,19 +6,28 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface StreamConfiguration : NSObject
@property NSString* host;
@property NSString* appVersion;
@property NSString* gfeVersion;
@property NSString* appID;
@property NSString* appName;
@property int width;
@property int height;
@property int frameRate;
@property int bitRate;
@property int riKeyId;
@property BOOL streamingRemotely;
@property NSData* riKey;
@property int gamepadMask;
@property BOOL optimizeGameSettings;
@property BOOL playAudioOnPC;
@property int audioChannelCount;
@property int audioChannelMask;
@property BOOL enableHdr;
@property BOOL multiController;
@property BOOL allowHevc;
@property NSData* serverCert;
@end
+1 -1
View File
@@ -9,5 +9,5 @@
#import "StreamConfiguration.h"
@implementation StreamConfiguration
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey;
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioChannelMask, audioChannelCount, enableHdr, multiController, allowHevc, serverCert;
@end
+2 -2
View File
@@ -6,13 +6,13 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "StreamConfiguration.h"
#import "Connection.h"
@interface StreamManager : NSOperation
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
- (void) stopStream;
@end
+27 -32
View File
@@ -10,7 +10,7 @@
#import "CryptoManager.h"
#import "HttpManager.h"
#import "Utils.h"
#import "OnScreenControls.h"
#import "StreamView.h"
#import "ServerInfoResponse.h"
#import "HttpResponse.h"
@@ -19,12 +19,13 @@
@implementation StreamManager {
StreamConfiguration* _config;
UIView* _renderView;
OSView* _renderView;
id<ConnectionCallbacks> _callbacks;
Connection* _connection;
}
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
self = [super init];
_config = config;
_renderView = view;
@@ -34,19 +35,16 @@
return self;
}
- (void)main {
[CryptoManager generateKeyPairUsingSSl];
[CryptoManager generateKeyPairUsingSSL];
NSString* uniqueId = [IdManager getUniqueId];
NSData* cert = [CryptoManager readCertFromFile];
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
uniqueId:uniqueId
deviceName:@"roth"
cert:cert];
serverCert:_config.serverCert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
NSString* pairStatus = [serverInfoResp getStringTag:@"PairStatus"];
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
@@ -75,22 +73,27 @@
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;
_config.gfeVersion = gfeVersion;
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc]initWithView:_renderView];
_connection = [[Connection alloc] initWithConfig:_config renderer:renderer connectionCallbacks:_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:_connection];
// Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView];
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection];
});
}
- (void) stopStream
@@ -100,21 +103,15 @@
- (BOOL) launchApp:(HttpManager*)hMan {
HttpResponse* launchResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:
[hMan newLaunchRequest:_config.appID
width:_config.width
height:_config.height
refreshRate:_config.frameRate
rikey:[Utils bytesToHex:_config.riKey]
rikeyid:_config.riKeyId]]];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchRequest:_config]]];
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
if (launchResp == NULL || ![launchResp isStatusOk]) {
[_callbacks launchFailed:@"Failed to launch app"];
if (![launchResp isStatusOk]) {
[_callbacks launchFailed:launchResp.statusMessage];
Log(LOG_E, @"Failed Launch Response: %@", launchResp.statusMessage);
return FALSE;
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
[_callbacks launchFailed:launchResp.statusMessage];
Log(LOG_E, @"Failed to parse game session. Code: %ld Response: %@", (long)launchResp.statusCode, launchResp.statusMessage);
[_callbacks launchFailed:@"Failed to launch app"];
Log(LOG_E, @"Failed to parse game session");
return FALSE;
}
@@ -123,17 +120,15 @@
- (BOOL) resumeApp:(HttpManager*)hMan {
HttpResponse* resumeResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:
[hMan newResumeRequestWithRiKey:[Utils bytesToHex:_config.riKey]
riKeyId:_config.riKeyId]]];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newResumeRequest:_config]]];
NSString* resume = [resumeResp getStringTag:@"resume"];
if (resumeResp == NULL || ![resumeResp isStatusOk]) {
[_callbacks launchFailed:@"Failed to resume app"];
if (![resumeResp isStatusOk]) {
[_callbacks launchFailed:resumeResp.statusMessage];
Log(LOG_E, @"Failed Resume Response: %@", resumeResp.statusMessage);
return FALSE;
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
[_callbacks launchFailed:resumeResp.statusMessage];
Log(LOG_E, @"Failed to parse resume response. Code: %ld Response: %@", (long)resumeResp.statusCode, resumeResp.statusMessage);
[_callbacks launchFailed:@"Failed to resume app"];
Log(LOG_E, @"Failed to parse resume response");
return FALSE;
}
+2 -4
View File
@@ -6,18 +6,16 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@import AVFoundation;
@interface VideoDecoderRenderer : NSObject
- (id)initWithView:(UIView*)view;
- (id)initWithView:(OSView*)view;
- (void)setupWithVideoFormat:(int)videoFormat;
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType;
@end
+62 -51
View File
@@ -7,11 +7,12 @@
//
#import "VideoDecoderRenderer.h"
#import "StreamView.h"
#include "Limelight.h"
@implementation VideoDecoderRenderer {
UIView *_view;
StreamView* _view;
AVSampleBufferDisplayLayer* displayLayer;
Boolean waitingForSps, waitingForPps, waitingForVps;
@@ -27,10 +28,15 @@
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
displayLayer.bounds = _view.bounds;
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
displayLayer.backgroundColor = [OSColor blackColor].CGColor;
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
// Hide the layer until we get an IDR frame. This ensures we
// can see the loading progress label as the stream is starting.
displayLayer.hidden = YES;
if (oldLayer != nil) {
// Switch out the old display layer with the new one
[_view.layer replaceSublayer:oldLayer with:displayLayer];
@@ -53,7 +59,7 @@
}
}
- (id)initWithView:(UIView*)view
- (id)initWithView:(StreamView*)view
{
self = [super init];
@@ -67,24 +73,18 @@
- (void)setupWithVideoFormat:(int)videoFormat
{
self->videoFormat = videoFormat;
#if !TARGET_OS_IPHONE
_view.codec = videoFormat;
#endif
}
#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4
#define AVC_NAL_TYPE_SPS 0x67
#define AVC_NAL_TYPE_PPS 0x68
#define HEVC_NAL_TYPE_VPS 0x40
#define HEVC_NAL_TYPE_SPS 0x42
#define HEVC_NAL_TYPE_PPS 0x44
#define IS_NAL_SPS(x) ((videoFormat == VIDEO_FORMAT_H264) ? ((x) == AVC_NAL_TYPE_SPS) : ((x) == HEVC_NAL_TYPE_SPS))
#define IS_NAL_PPS(x) ((videoFormat == VIDEO_FORMAT_H264) ? ((x) == AVC_NAL_TYPE_PPS) : ((x) == HEVC_NAL_TYPE_PPS))
#define IS_NAL_VPS(x) ((videoFormat == VIDEO_FORMAT_H265) && ((x) == HEVC_NAL_TYPE_VPS))
- (Boolean)readyForPictureData
{
if (videoFormat == VIDEO_FORMAT_H264) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return !waitingForSps && !waitingForPps;
}
else {
@@ -95,7 +95,7 @@
- (Boolean)isNalReferencePicture:(unsigned char)nalType
{
if (videoFormat == VIDEO_FORMAT_H264) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return nalType == 0x65;
}
else {
@@ -114,16 +114,6 @@
}
}
- (Boolean)isNalPictureData:(unsigned char)nalType
{
if (videoFormat == VIDEO_FORMAT_H264) {
return !(nalType == AVC_NAL_TYPE_SPS || nalType == AVC_NAL_TYPE_PPS);
}
else {
return !(nalType == HEVC_NAL_TYPE_VPS || nalType == HEVC_NAL_TYPE_SPS || nalType == HEVC_NAL_TYPE_PPS);
}
}
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
{
OSStatus status;
@@ -190,25 +180,14 @@
}
}
// This function must free data
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType
{
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
OSStatus status;
// Check for previous decoder errors before doing anything
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
// Recreate the display layer
[self reinitializeDisplayLayer];
// Request an IDR frame to initialize the new decoder
return DR_NEED_IDR;
}
if (![self isNalPictureData:nalType]) {
if (IS_NAL_VPS(nalType)) {
if (bufferType != BUFFER_TYPE_PICDATA) {
if (bufferType == BUFFER_TYPE_VPS) {
Log(LOG_I, @"Got VPS");
vpsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
waitingForVps = false;
@@ -216,14 +195,14 @@
// We got a new VPS so wait for a new SPS to match it
waitingForSps = true;
}
else if (IS_NAL_SPS(nalType)) {
else if (bufferType == BUFFER_TYPE_SPS) {
Log(LOG_I, @"Got SPS");
spsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
waitingForSps = false;
// We got a new SPS so wait for a new PPS to match it
waitingForPps = true;
} else if (IS_NAL_PPS(nalType)) {
} else if (bufferType == BUFFER_TYPE_PPS) {
Log(LOG_I, @"Got PPS");
ppsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
waitingForPps = false;
@@ -231,7 +210,7 @@
// See if we've got all the parameter sets we need for our video format
if ([self readyForPictureData]) {
if (videoFormat == VIDEO_FORMAT_H264) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
@@ -252,7 +231,12 @@
const size_t parameterSetSizes[] = { [vpsData length], [spsData length], [ppsData length] };
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,
@@ -273,8 +257,7 @@
}
}
// Free the data buffer
free(data);
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
// No frame data to submit for these NALUs
return DR_OK;
@@ -283,7 +266,24 @@
if (formatDesc == NULL) {
// Can't decode if we haven't gotten our parameter sets yet
free(data);
return DR_OK;
return DR_NEED_IDR;
}
// Check for previous decoder errors before doing anything
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
// Recreate the display layer on the main thread.
// We need to use dispatch_sync() or we may miss
// some parameter sets while the layer is being
// recreated.
dispatch_sync(dispatch_get_main_queue(), ^{
[self reinitializeDisplayLayer];
});
// Request an IDR frame to initialize the new decoder
free(data);
return DR_NEED_IDR;
}
// Now we're decoding actual frame data here
@@ -346,12 +346,23 @@
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
[displayLayer enqueueSampleBuffer:sampleBuffer];
// Dereference the buffers
CFRelease(blockBuffer);
CFRelease(sampleBuffer);
#if !TARGET_OS_IPHONE
_view.frameCount++;
#endif
// Enqueue video samples on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// Enqueue the next frame
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
// Ensure the layer is visible now
self->displayLayer.hidden = NO;
// Dereference the buffers
CFRelease(blockBuffer);
CFRelease(sampleBuffer);
});
return DR_OK;
}
+90 -16
View File
@@ -7,6 +7,9 @@
//
#import "UIAppView.h"
#import "AppAssetManager.h"
static const float REFRESH_CYCLE = 2.0f;
@implementation UIAppView {
TemporaryApp* _app;
@@ -31,22 +34,31 @@ static UIImage* noImage;
noImage = [UIImage imageNamed:@"NoAppImage"];
}
#if TARGET_OS_TV
_appButton = [UIButton buttonWithType:UIButtonTypeSystem];
#else
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
#endif
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_appButton sizeToFit];
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self addSubview:_appButton];
[self sizeToFit];
_appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
// Rasterizing the cell layer increases rendering performance by quite a bit
self.layer.shouldRasterize = YES;
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
[self updateAppImage];
[self startUpdateLoop];
return self;
}
@@ -60,40 +72,64 @@ static UIImage* noImage;
_appOverlay = nil;
}
_appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
if ([_app.id isEqualToString:_app.host.currentGame]) {
#if TARGET_OS_TV
_appButton.layer.masksToBounds = NO;
_appButton.layer.shadowColor = [UIColor greenColor].CGColor;
_appButton.layer.shadowOffset = CGSizeMake(5,8);
_appButton.layer.shadowOpacity = 0.7;
#else
// 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 = 2.0;
[self addSubview:_appOverlay];
_appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
#endif
}
// TODO: Improve no-app image detection
BOOL noAppImage = false;
if (_app.image != nil) {
// Load the decoded image from the cache
UIImage* appImage = [_artCache objectForKey:_app];
if (appImage == nil) {
// Not cached; we have to decode this now
appImage = [UIImage imageWithData:_app.image];
// First check the memory cache
UIImage* appImage = [_artCache objectForKey:_app];
if (appImage == nil) {
// Next try to load from the on disk cache
appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
if (appImage != nil) {
[_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
//custom image to do TvOS hover popup effect
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
imageView.userInteractionEnabled = YES;
imageView.adjustsImageWhenAncestorFocused = YES;
imageView.frame = CGRectMake(0, 0, 200, 265);
[_appButton addSubview:imageView];
_appButton.frame = CGRectMake(0, 0, 200, 265);
self.frame = CGRectMake(0, 0, 200, 265);
#else
_appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
self.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
_appOverlay.frame = CGRectMake(0, 0, self.frame.size.width / 2.f, self.frame.size.height / 4.f);
_appOverlay.layer.shadowRadius = 4.0;
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
#endif
[_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
[self setNeedsDisplay];
} else {
@@ -105,18 +141,56 @@ static UIImage* noImage;
if (noAppImage) {
_appLabel = [[UILabel alloc] init];
CGFloat padding = 4.f;
[_appLabel setFrame: CGRectMake(padding, padding, _appButton.frame.size.width - 2 * padding, _appButton.frame.size.height - 2 * padding)];
[_appLabel setTextColor:[UIColor whiteColor]];
[_appLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:10.f]];
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
[_appLabel setTextAlignment:NSTextAlignmentCenter];
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
[_appLabel setNumberOfLines:0];
[_appLabel setText:_app.name];
#if TARGET_OS_TV
[_appLabel setFont:[UIFont systemFontOfSize:16]];
[_appLabel setAdjustsFontSizeToFitWidth:YES];
[_appLabel setFrame: CGRectMake(0, 0, 200, 265)];
//custom image to do TvOS hover popup effect
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
imageView.userInteractionEnabled = YES;
imageView.adjustsImageWhenAncestorFocused = YES;
imageView.frame = CGRectMake(0, 0, 200, 265);
UIGraphicsBeginImageContextWithOptions(_appLabel.frame.size, false, 0);
[imageView.layer renderInContext:(UIGraphicsGetCurrentContext())];
[_appLabel.layer renderInContext:(UIGraphicsGetCurrentContext())];
UIImage *imageWithText = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[imageView setImage:imageWithText];
[_appButton addSubview:imageView];
_appButton.frame = CGRectMake(0, 0, 200, 265);
self.frame = CGRectMake(0, 0, 200, 265);
#else
CGFloat padding = 4.f;
[_appLabel setFrame: CGRectMake(padding, padding, _appButton.frame.size.width - 2 * padding, _appButton.frame.size.height - 2 * padding)];
[_appButton addSubview:_appLabel];
#endif
}
}
- (void) startUpdateLoop {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
- (void) updateLoop {
// Update the app image if neccessary
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
#if !TARGET_OS_TV
[self updateAppImage];
#endif
}
// Stop updating when we detach from our parent view
if (self.superview != nil) {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
}
@end
+45 -9
View File
@@ -18,7 +18,12 @@
CGSize _labelSize;
}
static const float REFRESH_CYCLE = 2.0f;
#if TARGET_OS_TV
static const int LABEL_DY = 40;
#else
static const int LABEL_DY = 20;
#endif
- (id) init {
self = [super init];
@@ -34,9 +39,7 @@ static const int LABEL_DY = 20;
_hostLabel = [[UILabel alloc] init];
_hostStatus = [[UILabel alloc] init];
_hostPairState = [[UILabel alloc] init];
[_hostLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
[_hostStatus setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
[_hostPairState setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
return self;
}
@@ -46,12 +49,19 @@ static const int LABEL_DY = 20;
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
}
[_hostButton sizeToFit];
[_hostLabel setText:@"Add Host"];
[_hostLabel sizeToFit];
#if !TARGET_OS_TV
_hostLabel.textColor = [UIColor whiteColor];
#endif
_hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
UIImageView* addIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AddComputerIcon"]];
@@ -77,9 +87,15 @@ static const int LABEL_DY = 20;
_host = host;
_callback = callback;
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked)];
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
[_hostButton addGestureRecognizer:longPressRecognizer];
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self updateContentsForHost:host];
[self updateBounds];
@@ -125,7 +141,9 @@ static const int LABEL_DY = 20;
- (void) updateContentsForHost:(TemporaryHost*)host {
_hostLabel.text = _host.name;
#if !TARGET_OS_TV
_hostLabel.textColor = [UIColor whiteColor];
#endif
[_hostLabel sizeToFit];
switch (host.pairState) {
@@ -139,7 +157,9 @@ static const int LABEL_DY = 20;
_hostPairState.text = @"Paired";
break;
}
#if !TARGET_OS_TV
_hostPairState.textColor = [UIColor whiteColor];
#endif
[_hostPairState sizeToFit];
if (host.online) {
@@ -163,11 +183,17 @@ static const int LABEL_DY = 20;
- (void) updateLoop {
[self updateContentsForHost:_host];
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
// Stop updating when we detach from our parent view
if (self.superview != nil) {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
}
- (void) hostLongClicked {
[_callback hostLongClicked:_host view:self];
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
[_callback hostLongClicked:_host view:self];
}
}
- (void) hostClicked {
@@ -178,4 +204,14 @@ 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;
previousButton.layer.shadowColor = [[UIColor blackColor] CGColor];
nextButton.layer.shadowColor = [[UIColor greenColor] CGColor];
}
#endif
@end
+8
View File
@@ -8,6 +8,8 @@
#import "Logger.h"
static LogLevel LoggerLogLevel = LOG_I;
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args);
void Log(LogLevel level, NSString* fmt, ...) {
@@ -27,6 +29,10 @@ void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...) {
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
NSString* levelPrefix = @"";
if (level < LoggerLogLevel) {
return;
}
switch(level) {
case LOG_D:
levelPrefix = PRFX_DEBUG;
@@ -41,6 +47,8 @@ void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
levelPrefix = PRFX_ERROR;
break;
default:
levelPrefix = @"";
assert(false);
break;
}
NSString* prefixedString;
+2 -3
View File
@@ -6,8 +6,6 @@
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
@interface Utils : NSObject
typedef NS_ENUM(int, PairState) {
@@ -22,6 +20,7 @@ FOUNDATION_EXPORT NSString *const deviceName;
+ (NSString*) bytesToHex:(NSData*)data;
+ (NSData*) hexToBytes:(NSString*) hex;
+ (int) resolveHost:(NSString*)host;
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
@end
@@ -29,4 +28,4 @@ FOUNDATION_EXPORT NSString *const deviceName;
- (NSString*) trim;
@end
@end
+9
View File
@@ -73,6 +73,15 @@ NSString *const deviceName = @"roth";
}
}
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
#if !TARGET_OS_TV
// tvOS doesn't have a browser
[dialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
}]];
#endif
}
@end
@implementation NSString (NSStringWithTrim)
@@ -10,5 +10,11 @@
@interface LoadingFrameViewController : UIViewController
- (void)showLoadingFrame:(void (^)(void))completion;
- (void)dismissLoadingFrame:(void (^)(void))completion;
- (BOOL)isShown;
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingSpinner;
@end
@@ -8,11 +8,9 @@
#import "LoadingFrameViewController.h"
@interface LoadingFrameViewController ()
@end
@implementation LoadingFrameViewController
@implementation LoadingFrameViewController {
BOOL presented;
};
- (void)viewDidLoad {
[super viewDidLoad];
@@ -20,19 +18,66 @@
self.loadingSpinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
- (UIViewController*) activeViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
while (topController.presentedViewController) {
topController = topController.presentedViewController;
}
return topController;
}
/*
#pragma mark - Navigation
- (void)showLoadingFrame:(void (^)(void))completion {
if (!presented) {
Log(LOG_I, @"Loading frame presenting start");
presented = YES;
[[self activeViewController] presentViewController:self animated:NO completion:^{
Log(LOG_I, @"Loading frame presenting complete");
if (completion) {
completion();
}
}];
}
else if (completion) {
Log(LOG_E, @"Loading frame already shown!");
completion();
}
}
// In a storyboard-based application, you will often want to do a little preparation before navigation
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
// Get the new view controller using [segue destinationViewController].
// Pass the selected object to the new view controller.
- (void)dismissLoadingFrame:(void (^)(void))completion {
if (presented) {
Log(LOG_I, @"Loading frame hiding start");
[self dismissViewControllerAnimated:NO completion:^{
Log(LOG_I, @"Loading frame hiding complete");
// Since presented is set to NO here rather than
// immediately in dismissLoadingFrame, we may
// falsely avoid displaying the loading frame if
// a dismiss is in progress while attempting to show
// the frame. That's preferable to crashing due to
// displaying the same VC twice though.
//
// This scenario can happen if the app is suspended
// while the dismiss is in progress then on resume
// it attempts to display it again before the dismiss
// completes. It can be reproduced by rapidly pressing
// Home and switching back to Moonlight while in the app grid.
// It reproduces more easily if the VC transitions are animated.
self->presented = NO;
if (completion) {
completion();
}
}];
}
else if (completion) {
completion();
}
}
- (BOOL)isShown {
return presented;
}
*/
@end
@@ -17,7 +17,9 @@
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
#if !TARGET_OS_TV
@property (strong, nonatomic) IBOutlet UIButton *limelightLogoButton;
@property (weak, nonatomic) IBOutlet UIBarButtonItem *computerNameButton;
#endif
@end
File diff suppressed because it is too large Load Diff
@@ -996,16 +996,32 @@ const int FrontViewPositionNone = 0xff;
- (void)_getRevealWidth:(CGFloat*)pRevealWidth revealOverDraw:(CGFloat*)pRevealOverdraw forSymetry:(int)symetry
{
if ( symetry < 0 ) *pRevealWidth = _rightViewRevealWidth, *pRevealOverdraw = _rightViewRevealOverdraw;
else *pRevealWidth = _rearViewRevealWidth, *pRevealOverdraw = _rearViewRevealOverdraw;
if ( symetry < 0 )
{
*pRevealWidth = _rightViewRevealWidth;
*pRevealOverdraw = _rightViewRevealOverdraw;
}
else
{
*pRevealWidth = _rearViewRevealWidth;
*pRevealOverdraw = _rearViewRevealOverdraw;
}
if (*pRevealWidth < 0) *pRevealWidth = _contentView.bounds.size.width + *pRevealWidth;
}
- (void)_getBounceBack:(BOOL*)pBounceBack pStableDrag:(BOOL*)pStableDrag forSymetry:(int)symetry
{
if ( symetry < 0 ) *pBounceBack = _bounceBackOnLeftOverdraw, *pStableDrag = _stableDragOnLeftOverdraw;
else *pBounceBack = _bounceBackOnOverdraw, *pStableDrag = _stableDragOnOverdraw;
if ( symetry < 0 )
{
*pBounceBack = _bounceBackOnLeftOverdraw;
*pStableDrag = _stableDragOnLeftOverdraw;
}
else
{
*pBounceBack = _bounceBackOnOverdraw;
*pStableDrag = _stableDragOnOverdraw;
}
}
- (void)_getAdjustedFrontViewPosition:(FrontViewPosition*)frontViewPosition forSymetry:(int)symetry
@@ -1371,8 +1387,15 @@ const int FrontViewPositionNone = 0xff;
// These are derived from the primary view controller
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
// iOS 11 betas (which are still installed in some places :\) crash with
// doesNotRecognizeSelector since this wasn't added until after the iPhone X
// announcement.
if ([self respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
if ([self respondsToSelector: @selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
}
}
@@ -1391,21 +1414,21 @@ const int FrontViewPositionNone = 0xff;
// Primitive method for view controller deployment and animated layout to the given position.
- (void)_setFrontViewPosition:(FrontViewPosition)newPosition withDuration:(NSTimeInterval)duration
{
void (^rearDeploymentCompletion)() = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
void (^rightDeploymentCompletion)() = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
void (^frontDeploymentCompletion)() = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
void (^rearDeploymentCompletion)(void) = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
void (^rightDeploymentCompletion)(void) = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
void (^frontDeploymentCompletion)(void) = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
void (^animations)() = ^()
void (^animations)(void) = ^(void)
{
// Calling this in the animation block causes the status bar to appear/dissapear in sync with our own animation
[self setNeedsStatusBarAppearanceUpdate];
// We call the layoutSubviews method on the contentView view and send a delegate, which will
// occur inside of an animation block if any animated transition is being performed
[_contentView layoutSubviews];
[self->_contentView layoutSubviews];
if ([_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
[_delegate revealController:self animateToPosition:_frontViewPosition];
if ([self->_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
[self->_delegate revealController:self animateToPosition:self->_frontViewPosition];
};
void (^completion)(BOOL) = ^(BOOL finished)
@@ -1448,21 +1471,31 @@ const int FrontViewPositionNone = 0xff;
UIView *view = nil;
if ( operation == SWRevealControllerOperationReplaceRearController )
old = _rearViewController, _rearViewController = new, view = _contentView.rearView;
{
old = _rearViewController;
_rearViewController = new;
view = _contentView.rearView;
}
else if ( operation == SWRevealControllerOperationReplaceFrontController )
old = _frontViewController, _frontViewController = new, view = _contentView.frontView;
{
old = _frontViewController;
_frontViewController = new;
view = _contentView.frontView;
}
else if ( operation == SWRevealControllerOperationReplaceRightController )
old = _rightViewController, _rightViewController = new, view = _contentView.rightView;
{
old = _rightViewController;
_rightViewController = new;
view = _contentView.rightView;
}
void (^completion)() = [self _transitionFromViewController:old toViewController:new inView:view];
void (^completion)(void) = [self _transitionFromViewController:old toViewController:new inView:view];
void (^animationCompletion)() = ^
void (^animationCompletion)(void) = ^
{
completion();
if ( [_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
[_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
if ( [self->_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
[self->_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
[self _dequeue];
};
@@ -1520,16 +1553,16 @@ const int FrontViewPositionNone = 0xff;
_frontViewPosition = newPosition;
void (^deploymentCompletion)() =
void (^deploymentCompletion)(void) =
[self _deploymentForViewController:_frontViewController inView:_contentView.frontView appear:appear disappear:disappear];
void (^completion)() = ^()
void (^completion)(void) = ^(void)
{
deploymentCompletion();
if ( positionIsChanging )
{
if ( [_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
[_delegate revealController:self didMoveToPosition:newPosition];
if ( [self->_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
[self->_delegate revealController:self didMoveToPosition:newPosition];
}
};
@@ -1645,11 +1678,11 @@ const int FrontViewPositionNone = 0xff;
if ( toController ) [self addChildViewController:toController];
void (^deployCompletion)() = [self _deployForViewController:toController inView:view];
void (^deployCompletion)(void) = [self _deployForViewController:toController inView:view];
[fromController willMoveToParentViewController:nil];
void (^undeployCompletion)() = [self _undeployForViewController:fromController];
void (^undeployCompletion)(void) = [self _undeployForViewController:fromController];
void (^completionBlock)(void) = ^(void)
{
@@ -15,6 +15,11 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hevcSelector;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
- (void) saveSettings;
@@ -10,17 +10,78 @@
#import "TemporarySettings.h"
#import "DataManager.h"
#define BITRATE_INTERVAL 500 // in kbps
@implementation SettingsViewController {
NSInteger _bitrate;
Boolean _adjustedForSafeArea;
}
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
static const int bitrateTable[] = {
500,
1000,
1500,
2000,
2500,
3000,
4000,
5000,
6000,
7000,
8000,
9000,
10000,
12000,
15000,
18000,
20000,
30000,
40000,
50000,
60000,
70000,
80000,
100000
};
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
-(int)getSliderValueForBitrate:(NSInteger)bitrate {
int i;
for (i = 0; i < (sizeof(bitrateTable) / sizeof(*bitrateTable)); i++) {
if (bitrate <= bitrateTable[i]) {
return i;
}
}
// Return the last entry in the table
return i - 1;
}
-(void)viewDidLayoutSubviews {
// On iPhone layouts, this view is rooted at a ScrollView. To make it
// scrollable, we'll update content size here.
if (self.scrollView != nil) {
CGFloat highestViewY = 0;
// Enumerate the scroll view's subviews looking for the
// highest view Y value to set our scroll view's content
// size.
for (UIView* view in self.scrollView.subviews) {
// UIScrollViews have 2 default UIImageView children
// which represent the horizontal and vertical scrolling
// indicators. Ignore these when computing content size.
if ([view isKindOfClass:[UIImageView class]]) {
continue;
}
CGFloat currentViewY = view.frame.origin.y + view.frame.size.height;
if (currentViewY > highestViewY) {
highestViewY = currentViewY;
}
}
// Add a bit of padding so the view doesn't end right at the button of the display
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width,
highestViewY + 20);
}
// Adjust the subviews for the safe area on the iPhone X.
if (!_adjustedForSafeArea) {
@@ -45,55 +106,106 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
DataManager* dataMan = [[DataManager alloc] init];
TemporarySettings* currentSettings = [dataMan getSettings];
// Bitrate is persisted in kbps
_bitrate = [currentSettings.bitrate integerValue];
NSInteger framerate = [currentSettings.framerate integerValue] == 30 ? 0 : 1;
NSInteger resolution;
if ([currentSettings.height integerValue] == 720) {
resolution = 0;
} else if ([currentSettings.height integerValue] == 1080) {
resolution = 1;
} else {
resolution = 0;
}
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
// Ensure we pick a bitrate that falls exactly onto a slider notch
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
NSInteger framerate;
switch ([currentSettings.framerate integerValue]) {
case 30:
framerate = 0;
break;
default:
case 60:
framerate = 1;
break;
case 120:
framerate = 2;
break;
}
NSInteger resolution;
switch ([currentSettings.height integerValue]) {
case 360:
resolution = 0;
break;
default:
case 720:
resolution = 1;
break;
case 1080:
resolution = 2;
break;
}
// Only show the 120 FPS option if we have a > 60-ish Hz display
bool enable120Fps = false;
if (@available(iOS 10.3, *)) {
if ([UIScreen mainScreen].maximumFramesPerSecond > 62) {
enable120Fps = true;
}
}
if (!enable120Fps) {
[self.framerateSelector removeSegmentAtIndex:2 animated:NO];
}
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
[self.hevcSelector setSelectedSegmentIndex:currentSettings.useHevc ? 1 : 0];
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
[self.resolutionSelector setSelectedSegmentIndex:resolution];
[self.resolutionSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.framerateSelector setSelectedSegmentIndex:framerate];
[self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
[self.bitrateSlider setValue:(_bitrate / BITRATE_INTERVAL) animated:YES];
[self.bitrateSlider setMinimumValue:0];
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateBitrateText];
}
- (void) newResolutionFpsChosen {
NSInteger frameRate = [self getChosenFrameRate];
NSInteger resHeight = [self getChosenStreamHeight];
NSInteger fps = [self getChosenFrameRate];
NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight];
NSInteger defaultBitrate;
// 1080p60 is 20 Mbps
if (frameRate == 60 && resHeight == 1080) {
defaultBitrate = 20000;
// This table prefers 16:10 resolutions because they are
// only slightly more pixels than the 16:9 equivalents, so
// we don't want to bump those 16:10 resolutions up to the
// next 16:9 slot.
//
// This logic is shamelessly stolen from Moonlight Qt:
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
if (width * height <= 640 * 360) {
defaultBitrate = 1000 * (fps / 30.0);
}
// 720p60 and 1080p30 are 10 Mbps
else if (frameRate == 60 || resHeight == 1080) {
defaultBitrate = 10000;
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
defaultBitrate = 5000 * (fps / 30.0);
}
// 720p30 is 5 Mbps
else {
defaultBitrate = 5000;
else if (width * height <= 1920 * 1200) {
defaultBitrate = 10000 * (fps / 30.0);
}
else if (width * height <= 2560 * 1600) {
defaultBitrate = 20000 * (fps / 30.0);
}
else /* if (width * height <= 3840 * 2160) */ {
defaultBitrate = 40000 * (fps / 30.0);
}
// We should always be exactly on a slider position with default bitrates
_bitrate = defaultBitrate;
[self.bitrateSlider setValue:defaultBitrate / BITRATE_INTERVAL animated:YES];
assert(bitrateTable[[self getSliderValueForBitrate:_bitrate]] == _bitrate);
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self updateBitrateText];
}
- (void) bitrateSliderMoved {
_bitrate = BITRATE_INTERVAL * (int)self.bitrateSlider.value;
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
[self updateBitrateText];
}
@@ -103,15 +215,26 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
}
- (NSInteger) getChosenFrameRate {
return [self.framerateSelector selectedSegmentIndex] == 0 ? 30 : 60;
switch ([self.framerateSelector selectedSegmentIndex]) {
case 0:
return 30;
case 1:
return 60;
case 2:
return 120;
default:
abort();
}
}
- (NSInteger) getChosenStreamHeight {
return [self.resolutionSelector selectedSegmentIndex] == 0 ? 720 : 1080;
const int resolutionTable[] = { 360, 720, 1080 };
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]];
}
- (NSInteger) getChosenStreamWidth {
return [self getChosenStreamHeight] == 720 ? 1280 : 1920;
// Assumes fixed 16:9 aspect ratio
return ([self getChosenStreamHeight] * 16) / 9;
}
- (void) saveSettings {
@@ -120,7 +243,21 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
NSInteger height = [self getChosenStreamHeight];
NSInteger width = [self getChosenStreamWidth];
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
[dataMan saveSettingsWithBitrate:_bitrate framerate:framerate height:height width:width onscreenControls:onscreenControls];
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
BOOL useHevc = [self.hevcSelector 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];
}
- (void)didReceiveMemoryWarning {

Some files were not shown because too many files have changed in this diff Show More