Compare commits

...
184 Commits
Author SHA1 Message Date
Cameron Gutman 02dc978049 Switch to GitHub Actions 2026-09-26 16:07:06 -05:00
Cameron Gutman 064723092b Stringify YUV 4:4:4 video formats 2026-09-26 16:01:17 -05:00
Cameron Gutman ca0efbdb5b Distinguish between Enter and Numpad Enter 2026-09-26 16:00:45 -05:00
Cameron Gutman 5cc4076d94 Update project to Xcode 27 recommended settings 2026-09-26 15:47:11 -05:00
Cameron Gutman 938f4b4589 Update moonlight-common-c 2026-09-26 15:45:47 -05:00
Cameron Gutman 7c79fcc6a2 Raise deployment target for Xcode 27 2026-09-26 15:38:07 -05:00
Cameron Gutman 159ffdbfde Update OpenSSL to 3.6.3 2026-09-26 14:13:39 -05:00
Cameron Gutman a52f344d00 Add section 7 exception for app store distribution
Since this code has always been exclusively distributed on the Apple App Store, the presumption is that all contributors implicitly consented to this exception.
2026-09-26 14:09:59 -05:00
Michael Pishchagin 85af0f7562 Add Game Mode support with LSSupportsGameMode
- Added LSSupportsGameMode=true to both iOS and tvOS Info.plist files
- LSSupportsGameMode is supported on iOS 18.6+/iPadOS 18.6+/macOS 26.0+
- GCSupportsGameMode key is marked as deprecated, so I haven't added it
2025-10-08 21:56:42 -05:00
Bogdan Seniuc da47035525 Override default SDL audio ducking option 2024-12-20 15:46:19 -06:00
Cameron Gutman db9a4ab7c2 Initialize block variables 2024-12-20 15:40:30 -06:00
Cameron Gutman a528ce7e2a Roll back package metadata version to fix CI 2024-12-20 15:34:17 -06:00
Cameron Gutman 1e0714ce70 Fix reference cycle in HttpManager due to delegate retention
Fixes #654
2024-12-20 15:33:02 -06:00
Cameron Gutman 997525c71e Fix leaks during pairing
Ref: #654
2024-12-20 15:31:45 -06:00
Cameron Gutman b88cf14b4e Fix leaks from getClientCertificate
Ref: #654
2024-12-20 15:31:12 -06:00
Cameron Gutman 1cfabac4b5 Disable certificate encryption to avoid legacy provider requirement
Closes #646
Fixes #660
2024-12-20 14:43:04 -06:00
Cameron Gutman 42518301f7 Update OpenSSL package 2024-12-20 14:18:15 -06:00
alexhaugland 022352c166 Upgrade OpenSSL so it works on visionOS (#635)
* Switch OpenSSL so it works on visionOS
* Fix cert generation to work with OpenSSL 3
* Update gitignore
2024-07-10 19:51:16 -05:00
Cameron Gutman 557765f2c0 Version 9.0.2 2024-02-25 16:46:42 -06:00
Cameron Gutman c3ad7578fe Update moonlight-common-c to resolve build warning 2024-02-25 16:44:47 -06:00
Cameron Gutman 7df349ad7c Improve smooth scrolling precision 2024-02-25 16:25:56 -06:00
Marc Davis 855f48b455 mapped the Globe/Language key to Escape 2024-02-25 15:49:48 -06:00
Marc Davis dd5d233993 yes 0x0D is the correct code for Enter according to Microsoft's documentation 2024-02-25 15:49:48 -06:00
Cameron Gutman 796f87713f Update moonlight-common-c with RTSP encryption and QoS improvements 2024-02-25 15:44:35 -06:00
Cameron Gutman 38259af3f1 Remove xcpretty in CI 2024-02-17 17:17:05 -06:00
Cameron Gutman 08279d825c Update AppVeyor images 2024-02-17 14:40:01 -06:00
Cameron Gutman 7465835808 Remove CPU core count check
Decryption seems fast enough for all reasonable bitrates even on dual-core devices.
2024-01-28 14:37:03 -06:00
Cameron Gutman 303303bf6b Update moonlight-common-c with finalized encryption changes 2024-01-19 19:18:21 -06:00
Cameron Gutman 9b28981b59 Opt in for video encryption on A10 and later SoCs 2024-01-15 15:21:56 -06:00
Cameron Gutman 5501d85762 Update moonlight-common-c with new encryption support 2024-01-15 15:11:32 -06:00
Cameron Gutman cc799768ef Fix signing metadata for iOS 2023-12-29 23:36:40 -06:00
Cameron Gutman ae1c290b79 Version 9.0.1 2023-12-29 22:33:58 -06:00
Cameron Gutman 62c6661267 Update moonlight-common-c for X-SS-Connect-Data support 2023-12-29 22:32:39 -06:00
Cameron Gutman 485c5e10db Update moonlight-common-c with fix for v4-mapped v6 addresses 2023-12-28 16:13:36 -06:00
Cameron Gutman e69cd3dbbc Define __APPLE_USE_RFC_3542 at the project level for IPV6_PKTINFO
For some reason, the definition in unix.c is not working as expected.
2023-12-27 22:27:09 -06:00
Cameron Gutman e05a6e4ec1 Update moonlight-common.xcodeproj to recommended build settings 2023-12-27 22:17:22 -06:00
Cameron Gutman 77485f2d8d Display error codes when the connection is terminated 2023-12-22 14:46:23 -06:00
Cameron Gutman 951289a802 Don't consume stylus events unless they're supported by the host
Fixes #595
2023-12-22 14:03:22 -06:00
Cameron Gutman 6f40922704 Version 9.0.0 2023-12-06 21:12:50 -06:00
Cameron Gutman 27ac6c0128 Set Game application category 2023-12-06 20:34:33 -06:00
Cameron Gutman 1f543325f8 Rewrite the OSC merging logic 2023-12-06 19:42:51 -06:00
Cameron Gutman 73d4199917 Implement immediate controller arrival and battery events 2023-12-06 19:12:04 -06:00
Cameron Gutman e0808d6bfc Remove codec options that are not supported 2023-12-03 20:30:40 -06:00
Cameron Gutman 86919bd263 Enable reference frame invalidation for AV1 2023-12-03 20:21:41 -06:00
Cameron Gutman 401d9fbae7 Update moonlight-common-c with new bitrate logic 2023-12-03 20:21:12 -06:00
Cameron Gutman bbc89011d4 Don't hardcode server signature length 2023-11-29 23:04:44 -06:00
Cameron Gutman a2f470ddeb Raise toggleable keys upon dismissal of keyboard
Fixes #591
2023-11-29 21:16:09 -06:00
Cameron Gutman b20079a165 Fix build warning on tvOS 2023-11-06 19:21:38 -06:00
Cameron Gutman 01b349d8d0 Check that a sequence header OBU was present in the IDR frame 2023-11-06 19:18:41 -06:00
Cameron Gutman 089b424f65 Rework CMVideoFormatDescription handling to fix several bugs
- Populate CMVideoFormatDescription with data parsed directly from sequence header OBU
- Recreate AV1 CMVideoFormatDescription for each IDR frame (same as H.264/HEVC) to fix HDR-SDR transitions
- Fixed leaking the format description on each IDR frame
2023-11-06 19:09:56 -06:00
Cameron Gutman c51caba1ec Rebuild FFmpeg with AV1 CBS compiled in 2023-11-06 19:00:13 -06:00
Cameron Gutman cb26398414 Download required platforms at build-time if not present 2023-11-04 03:44:25 -05:00
Cameron Gutman 8cd931cf50 Run tvOS builds on Big Sur until AppVeyor fixes Xcode on the Monterey image 2023-11-04 03:29:52 -05:00
Cameron Gutman 06fc3ec5dc Update CI to macOS Monterey image 2023-11-04 03:23:47 -05:00
Cameron Gutman 3ebcc86f0d Fix simulator builds and update to libopus v1.4
- Split library search paths into simulator and native targets
- Fixed incorrectly removed arm64 split from SDL simulator libraries
- Added simulator builds for opus libraries
2023-11-04 03:15:59 -05:00
Cameron Gutman b81ea6b874 Write the AV1CodecConfigurationBox data into the CMFormatDescription extensions 2023-11-04 01:10:35 -05:00
Cameron Gutman 75a7b9e477 Add guards around AV1 checks 2023-11-04 01:08:56 -05:00
Cameron Gutman ce25a66dc5 Bump SDL to 2.28.5 and add FFmpeg for AV1 parsing 2023-11-04 01:08:23 -05:00
Cameron Gutman 9366c763d6 Update moonlight-common-c with multi-homed host fix 2023-11-03 23:14:51 -05:00
Cameron Gutman 6974cda328 Extend codec configuration option for AV1 support 2023-11-03 23:13:44 -05:00
Cameron Gutman 0a59ce0ca9 Create a basic AV1 extension dictionary
We may also need the AV1CodecConfigurationBox though.
2023-10-20 17:11:02 -05:00
Cameron Gutman 628252ed28 Update moonlight-common-c 2023-10-20 17:04:15 -05:00
Cameron Gutman 2c76654841 Refactor decoder interface to pass decode unit into VideoDecoderRenderer 2023-10-20 17:04:06 -05:00
Cameron Gutman f20d90791a Change AV1 bitrate multiplier to match HEVC 2023-10-15 15:37:45 -05:00
Cameron Gutman 2df60e571c Add support for H.264/HEVC bitstreams with multiple sets of parameter set NALUs 2023-10-13 23:39:26 -05:00
Cameron Gutman 0879bf0583 Update moonlight-common-c 2023-10-13 23:38:11 -05:00
Cameron Gutman 7c77a385cf Don't hit test against invisible controls
Fixes #568
2023-09-29 02:17:32 -05:00
Cameron Gutman b9dbfdd82f Offset WoL ports when the host is using alternate ports 2023-09-29 01:52:47 -05:00
Cameron Gutman 5d9fa4d003 Revert "iPadOS External Display Fullscreen Support (#557)"
This breaks App Store Guidelines because we don't support all device orientations.

This reverts commit ff40bc105e.
2023-09-29 01:35:23 -05:00
Cameron Gutman 40938d198f Sync bitrate logic with Moonlight Qt 2023-09-29 01:28:02 -05:00
Cameron Gutman a46e7fa100 Link to a website we control rather than directly to Nvidia's knowledge base 2023-09-29 01:27:33 -05:00
Cameron Gutman 043bf735e8 Fix a few bugs in the new resolution options and remove dead code 2023-09-29 01:23:35 -05:00
Cameron Gutman a342c294a1 Swap safe area and full resolution options to keep ascending order 2023-09-29 01:08:18 -05:00
goob47 f8a76e4584 Add support for full screen and safe area resolutions (#577)
Display full width x height in label below the resolution switcher
2023-09-29 01:03:47 -05:00
Cameron Gutman d6ee43dab5 Fixups for keyboard toolbar feature 2023-09-29 00:50:35 -05:00
goob47 884deb9244 Add a toolbar above the on-screen keyboard with extra keys that are missing from iOS keyboard (#576)
* Add required images to assets

* Add toolbar above keyboard with more key options

* Fix tvOS errors
2023-09-29 00:49:35 -05:00
Cameron Gutman 514e415956 Add handling for Apple TV D-Pad
Fixes #569
2023-09-28 23:54:28 -05:00
Cameron Gutman 95dcbf6024 Add workaround for old Xcode version in AppVeyor 2023-09-28 23:37:36 -05:00
Cameron Gutman 9d0dc49fd2 Implement controller LED support 2023-09-27 08:52:26 -05:00
Cameron Gutman 7249854641 Rework gamepad handling
- Fix accelerometer axis direction
- Fix touchpad input coordinates
- Use physicalInputProfile instead of casting to specific GC classes
- Remove deprecated GCGamepad support
2023-09-27 07:12:36 -05:00
Cameron Gutman 11da9e0eea Add native Apple Pencil support using Sunshine protocol extensions 2023-09-27 05:54:17 -05:00
Cameron Gutman 4f9eb6ea04 Implement host processing time in stats overlay 2023-09-27 03:57:49 -05:00
Cameron Gutman f883f5a2b5 Fix axis values for gyro input 2023-09-27 03:42:05 -05:00
Cameron Gutman 3fb328e238 Fix for old Xcode version in CI 2023-09-24 20:55:08 -04:00
empireslayer000 ff40bc105e iPadOS External Display Fullscreen Support (#557) 2023-09-24 20:48:42 -04:00
Cameron Gutman 957ce6095e Basic AV1 plumbing 2023-09-18 21:24:12 -05:00
Cameron Gutman d1c35144e1 Update moonlight-common-c 2023-09-18 21:20:45 -05:00
Cameron Gutman 65bf4ca6b1 Fix controller touch events being sent as normal touch events 2023-07-01 01:32:00 -05:00
Cameron Gutman ef5c3d36d3 Implement controller motion sensor support
Axis values probably still need to be swapped around to match other platforms
2023-06-29 00:35:35 -05:00
Cameron Gutman d48690b320 Implement controller touchpad support 2023-06-29 00:17:43 -05:00
Cameron Gutman b279caa00b Fix touchpad button flag 2023-06-28 23:51:24 -05:00
Cameron Gutman cc16186eed Implement extended gamepad buttons and controller metadata support 2023-06-28 23:46:39 -05:00
Cameron Gutman 20d0087bdc Plumb trigger rumble and motion sensor callbacks 2023-06-28 21:27:46 -05:00
Cameron Gutman 4f3f27287c Remove legacy GCGamepad profile 2023-03-05 22:49:06 -06:00
Cameron Gutman 9f7ebdc771 Version 8.5.0 2023-03-05 22:47:48 -06:00
Cameron Gutman ba723816a1 Deregister GCMouse scroll callbacks 2023-03-05 19:45:29 -06:00
Cameron Gutman c57e89a0bd Implement Start+Select+L1+R1 shortcut to quit
Fixes #548
2023-03-05 19:10:28 -06:00
Cameron Gutman 7ddf4e12ed Implement horizontal scrolling for Apple TV 2023-03-05 18:57:58 -06:00
Cameron Gutman 4f265cd09f Rebuild libopus without LTO and float approximations 2023-03-05 15:25:44 -06:00
Cameron Gutman 69081367e4 Update to libopus 1.3.1 2023-03-05 14:57:30 -06:00
Cameron Gutman 83fd8225e4 Add GameStream EOL link 2023-03-05 14:19:32 -06:00
Cameron Gutman 2c422c77fb Add Sunshine note to pairing dialog 2023-03-05 14:17:23 -06:00
Cameron Gutman 5c47cb8908 Update to OpenSSL 1.1.1t 2023-03-05 14:15:15 -06:00
Cameron Gutman 924e79d00f Update moonlight-common-c 2023-03-04 12:12:35 -06:00
Cameron Gutman bdb7d08c57 Don't request IDR frame if the HDR metadata change was a no-op 2023-03-04 12:11:33 -06:00
Cameron Gutman 64803d9715 Tell the host if we want gamepads to persist after disconnection 2023-02-20 16:32:01 -06:00
oxkenshin 2462126963 Fix mouse speed too slow on iPad (#532)
* Fix mouse speed too slow on iPad

Fixes #491
2023-02-20 16:02:58 -06:00
Cameron Gutman 0e40e4795a Implement horizontal scrolling 2023-02-20 16:00:03 -06:00
Cameron Gutman 5c844d3d1f Pass SS_KBE_FLAG_NON_NORMALIZED when sending key events from characters 2023-02-03 00:01:18 -06:00
Cameron Gutman 6d7a8df54f Downgrade CI to Big Sur to fix missing tvOS targets 2023-02-02 23:57:18 -06:00
Cameron Gutman 9eae79f69c Add Sunshine references 2023-02-02 23:35:10 -06:00
Cameron Gutman a04e1ebb3b Consolidate launch and resume handling for Sunshine 2023-02-02 23:13:59 -06:00
Cameron Gutman dbb3087078 Don't suppress high refresh rates with Sunshine 2023-02-02 23:13:09 -06:00
Cameron Gutman eefe8522c4 Plumb HDR metadata from Sunshine 2023-02-02 23:11:27 -06:00
Cameron Gutman cf665ec774 Try to fix CI 2023-02-02 22:16:25 -06:00
Cameron Gutman da4f41291e Fix handling of older IPv6 addresses 2023-02-02 22:12:45 -06:00
Cameron Gutman 9a03dd184a Fix infinite loop of retries when multiple hosts are not resolving 2023-02-02 21:56:26 -06:00
Cameron Gutman 4f03dd8c08 Implement support for alternate ports with Sunshine
Fixes #529
2023-02-02 21:54:52 -06:00
Yannick B a2b15ed2ac Adding more Bitrate options to tvOS
Added bitrate value options of 70 and 80 Mbps in the Root.plist file specific to Moonlight TV.
2023-02-02 20:55:56 -06:00
Cameron Gutman 1c90ee607a Bump tvOS version too 2022-12-06 17:34:15 -06:00
Cameron Gutman 4f7b81335d Version 8.4.1 2022-12-06 17:25:13 -06:00
Cameron Gutman eab25181e3 Update moonlight-common-c to fix streaming from Sunshine 2022-12-06 17:23:44 -06:00
Cameron Gutman 9b234bc867 Version 8.4.0 2022-12-04 17:34:00 -06:00
Cameron Gutman c7badef9d7 Allow pairing with Sunshine hosts while streaming 2022-12-04 16:54:37 -06:00
Cameron Gutman 365f61b393 Fix undo menu appearing when opening the keyboard on iOS 16 2022-12-02 22:54:08 -06:00
Cameron Gutman 02e088ddb2 Fix suppression of right clicks when activating the keyboard 2022-12-02 22:03:42 -06:00
Cameron Gutman be0c071fb5 Update moonlight-common-c 2022-12-02 20:41:40 -06:00
Felipe Cavalcanti 71b953cdd2 Fix black screen in HDR mode in newer AppleTV 4K Models 2022-12-02 20:39:29 -06:00
Cameron Gutman 3494962bc0 Version 8.3.1 2022-10-09 17:52:54 -05:00
Cameron Gutman 7c65bfcc77 Update moonlight-common-c for speculative RFI support 2022-10-09 17:34:17 -05:00
Cameron Gutman bff3fd5f6c Version 8.3.0 2022-10-04 21:04:18 -05:00
Cameron Gutman c8907b8ab8 Add special error text for ML_ERROR_FRAME_CONVERSION 2022-10-04 20:32:23 -05:00
Cameron Gutman 66f8b6f40c Update moonlight-common-c with additional GFE 3.26 fixes 2022-10-04 20:23:26 -05:00
Cameron Gutman 6eaf9274b8 Upgrade project to Xcode 14 defaults
This drops support for iOS 11 and below (which appears to have already happened on the App Store side anyway)
2022-10-04 20:22:37 -05:00
Cameron Gutman dbc83234e9 Update moonlight-common-c 2022-10-03 23:10:56 -05:00
Cameron Gutman 054b3ae45e Enable RFI for HEVC only 2022-10-02 23:20:59 -05:00
Cameron Gutman 1f5ecdf1ea Default to HEVC enabled on iOS 2022-10-02 23:15:16 -05:00
Cameron Gutman 490abc320c Version 8.2.0 2022-09-28 18:17:51 -05:00
Cameron Gutman f99381df81 Only enable RFI at resolutions at or below 1080p due to bugs 2022-09-28 17:59:18 -05:00
Cameron Gutman 842b6b3c76 Remove stale function definition 2022-09-28 17:57:44 -05:00
Cameron Gutman f9a2eb022f Display HDR/SDR status in the performance overlay 2022-09-28 17:43:38 -05:00
Cameron Gutman d896101ed2 Try to recover if the frame header parsing fails 2022-09-28 17:43:17 -05:00
Cameron Gutman 2ae79c5827 Rewrite buffer patching logic to avoid leaking if the first NALU has an unexpected offset 2022-09-28 17:42:48 -05:00
Cameron Gutman c21f638399 Fix large frame header size for GFE 3.26 2022-09-23 00:03:46 -05:00
Cameron Gutman 6d94c8515e Fix mishandling of IDR frames with a SEI or AUD NAL 2022-09-12 00:05:55 -05:00
Cameron Gutman 65e40279ea Fix handling of 3 byte Annex B start sequences 2022-09-05 18:55:16 -05:00
Starlank 0bd81b8261 Added 1440p resolution option 2022-08-17 22:38:00 -05:00
Cameron Gutman ac801e9bfb Re-enable the tvOS's HEVC option (defaulted to enabled)
Fixes #481
2022-08-01 20:01:41 -05:00
Cameron Gutman 6678488edc Implement ABXY swap for on-screen controls 2022-07-21 20:54:57 -05:00
Cameron Gutman a80fa5cfbb Fix some minor issues with ABXY swap feature 2022-07-21 20:36:31 -05:00
IonBlade2K b9e0b28adc Add setting to swap A<->B and X<->Y buttons (#513)
* Added toggle for swapping A<->B and X<->Y buttons to enable proper button mapping for combined Joy-Cons in iOS16, where iOS level remapping is not supported and pushing Switch B (bottom face button) sends Xbox B (right face button) instead of Xbox A (bottom face button), etc.  Allows user to select between current behavior (button letter matches output) and muscle-memory behavior (face button pressed matches location of face button output)
2022-07-21 20:09:22 -05:00
Cameron Gutman eb801b553f Use newer AppVeyor machine image 2022-05-18 00:39:24 -05:00
Cameron Gutman 9cc7972d11 Update README.md 2022-04-05 19:52:30 -05:00
Cameron Gutman 1b2a2ebddf Version 8.1.0 (iOS) 2022-03-27 15:57:18 -05:00
Cameron Gutman 3b0d80bce2 Fix crash during display layer reinitialization 2022-03-27 14:14:19 -05:00
Cameron Gutman 9ea5e2ec99 Fix CI build 2022-03-27 13:34:06 -05:00
Cameron Gutman e79faba87d Ensure the HEVC selector is set appropriately upon startup 2022-03-27 13:29:51 -05:00
Cameron Gutman 11b180d1e3 Fix HDR selector state upon app restart 2022-03-27 13:25:04 -05:00
Cameron Gutman 54d1851c78 Add HDR toggle for iOS 2022-03-27 13:18:03 -05:00
Cameron Gutman 0d57bd1015 Version 8.0.0 2022-02-13 15:15:31 -06:00
Cameron Gutman 3a77bf3e65 Implement dynamic display mode changes for Apple TV 2022-02-13 14:27:17 -06:00
Cameron Gutman d7673075b2 Build with iOS 15 SDK on the BigSur AppVeyor image 2022-02-12 20:07:25 -06:00
Cameron Gutman 6fb6b9bf38 Don't pace frame delivery if the display refresh rate has dropped 2022-02-12 19:14:06 -06:00
Cameron Gutman 6435afd229 Remove per-app HDR support check
It doesn't seem to make a difference anymore whether it's supported or not.
GFE seems happy to enter HDR mode anyway.
2022-02-07 20:05:58 -06:00
Cameron Gutman bfdfdc3050 Enforce 256x256 minimum for streaming 2022-02-06 21:09:22 -06:00
Cameron Gutman d12c103117 Additional custom resolution limits and warning text 2022-02-06 20:40:55 -06:00
Cameron Gutman e3bce9cec3 Additional wording tweaks to frame pacing settings 2022-02-06 20:12:06 -06:00
Cameron Gutman fc73663253 Improve custom resolution support 2022-02-06 19:49:04 -06:00
hbouhadji b7ba94f77a ui fix resolutionSelector auto width 2022-02-06 19:07:11 -06:00
hbouhadji 29ba055024 dialog for custom resolution 2022-02-06 19:07:11 -06:00
hbouhadji 7f7770a42c Added support for native stream resolution 2022-02-06 19:06:58 -06:00
Cameron Gutman 77c6ca2993 Add scroll view to iPadOS settings 2022-02-06 18:48:17 -06:00
Cameron Gutman edf449b708 Cap selected channel count at physical maximum 2022-02-06 18:00:14 -06:00
Cameron Gutman 72292dde94 Fix iOS build 2022-02-06 17:59:32 -06:00
Cameron Gutman ff4f9e2bcb Tweak wording on surround options 2022-02-06 17:59:22 -06:00
Nitropud 701c83d79c Fix for: Surround sound audio on Apple TV #371
Added audio configuration option to the settings menu to allow users to select their speaker setup.
2022-02-06 17:52:06 -06:00
Cameron Gutman 87be7080cb Tweak frame pacing option text to be more clear 2022-02-06 17:35:53 -06:00
Cameron Gutman 736485616c Merge branch 'vsync' 2022-02-06 17:13:03 -06:00
Cameron Gutman f21c58306e Preserve stream aspect ratio even when host resolution changes 2022-02-06 16:57:23 -06:00
Felipe Cavalcanti 7d6cb247b8 Add Frame Pacing feature 2022-02-02 13:37:07 -03:00
Cameron Gutman bd582aa6c0 Switch to a pull-based renderer and render from CADisplayLink callback 2022-01-22 16:20:31 -06:00
Cameron Gutman 445c026ea9 Add xcschemes to .gitignore 2022-01-22 15:53:37 -06:00
Cameron Gutman bd313d97cb Clean up new audio code 2022-01-22 15:49:52 -06:00
Cameron Gutman 5055a6db1d Fix SDL initialization on tvOS 2022-01-14 19:01:03 -06:00
455 changed files with 45465 additions and 38097 deletions
+45
View File
@@ -0,0 +1,45 @@
name: CI
on:
push:
pull_request:
branches:
- master
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
build:
name: Build (${{ matrix.platform }})
runs-on: macos-latest
strategy:
fail-fast: false
matrix:
include:
- scheme: Moonlight
sdk: iphonesimulator
platform: iOS
- scheme: Moonlight TV
sdk: appletvsimulator
platform: tvOS
steps:
- name: Checkout
uses: actions/checkout@v7
with:
submodules: recursive
- name: Download ${{ matrix.platform }} platform
run: xcodebuild -downloadPlatform ${{ matrix.platform }}
- name: Build Debug
run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Debug -sdk ${{ matrix.sdk }} clean build
- name: Build Release
run: xcodebuild -project Moonlight.xcodeproj -scheme "${{ matrix.scheme }}" -configuration Release -sdk ${{ matrix.sdk }} clean build
+2
View File
@@ -1,4 +1,6 @@
**/xcuserdata/
**/Moonlight.xcscmblueprint
**/xcschemes/*.xcscheme
.DS_Store
Build
DerivedData
-144
View File
@@ -1,144 +0,0 @@
#!/bin/bash
set -e
source config.sh
# Number of CPUs (for make -j)
NCPU=`sysctl -n hw.ncpu`
if test x$NJOB = x; then
NJOB=$NCPU
fi
PLATFORMBASE=$(xcode-select -print-path)"/Platforms"
SCRIPT_DIR=$( (cd -P $(dirname $0) && pwd) )
DIST_DIR_BASE=${DIST_DIR_BASE:="$SCRIPT_DIR/dist"}
if [ ! -d opus ]
then
echo "opus source directory does not exist, run sync.sh"
fi
# PATH=${SCRIPT_DIR}/gas-preprocessor/:$PATH
for ARCH in $ARCHS
do
OPUS_DIR=opus-$ARCH
if [ ! -d $OPUS_DIR ]
then
echo "Directory $OPUS_DIR does not exist, run sync.sh"
exit 1
fi
echo "Compiling source for $ARCH in directory $OPUS_DIR"
echo "cd $OPUS_DIR"
cd $OPUS_DIR
DIST_DIR=$DIST_DIR_BASE-$ARCH
echo "mkdir -p $DIST_DIR"
mkdir -p $DIST_DIR
CFLAGS_ARCH=$ARCH
case $ARCH in
armv7)
EXTRA_FLAGS="--with-pic --enable-fixed-point"
EXTRA_CFLAGS="-mcpu=cortex-a8 -mfpu=neon -miphoneos-version-min=7.1"
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
IOSSDK=iPhoneOS
;;
armv7s)
EXTRA_FLAGS="--with-pic --enable-fixed-point"
EXTRA_CFLAGS="-mcpu=cortex-a9 -mfpu=neon -miphoneos-version-min=7.1"
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
IOSSDK=iPhoneOS
;;
aarch64)
CFLAGS_ARCH="arm64"
EXTRA_FLAGS="--with-pic --enable-fixed-point"
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
PLATFORM="${PLATFORMBASE}/iPhoneOS.platform"
IOSSDK=iPhoneOS
;;
x86_64)
EXTRA_FLAGS="--with-pic"
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
IOSSDK=iPhoneSimulator
;;
i386)
EXTRA_FLAGS="--with-pic"
EXTRA_CFLAGS="-miphoneos-version-min=7.1"
PLATFORM="${PLATFORMBASE}/iPhoneSimulator.platform"
IOSSDK=iPhoneSimulator
;;
*)
echo "Unsupported architecture ${ARCH}"
exit 1
;;
esac
echo "Configuring opus for $ARCH..."
echo "./autogen.sh"
./autogen.sh
CFLAGS="-g -O2 -pipe -arch ${CFLAGS_ARCH} \
-isysroot ${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
-I${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk/usr/include \
${EXTRA_CFLAGS}"
LDFLAGS="-arch ${CFLAGS_ARCH} \
-isysroot ${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
-L${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk/usr/lib"
echo "CFLAGS=$CFLAGS"
echo "LDFLAGS=$LDFLAGS"
export CFLAGS
export LDFLAGS
export CXXCPP="/usr/bin/cpp"
export CPP="$CXXCPP"
export CXX="/usr/bin/gcc"
export CC="/usr/bin/gcc"
export LD="/usr/bin/ld"
export AR="/usr/bin/ar"
export AS="/usr/bin/ls"
export NM="/usr/bin/nm"
export RANLIB="/usr/bin/ranlib"
export STRIP="/usr/bin/strip"
echo "./configure \
--prefix=$DIST_DIR \
--host=${ARCH}-apple-darwin \
--with-sysroot=${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
--enable-static=yes \
--enable-shared=no \
--disable-doc \
${EXTRA_FLAGS}"
./configure \
--prefix=$DIST_DIR \
--host=${ARCH}-apple-darwin \
--with-sysroot=${PLATFORM}/Developer/SDKs/${IOSSDK}.sdk \
--enable-static=yes \
--enable-shared=no \
--disable-doc \
${EXTRA_FLAGS}
echo "Installing opus for $ARCH..."
echo "make clean"
make clean
echo "make -j$NJOB V=1"
make -j$NJOB V=1
echo "make install"
make install
echo "cd $SCRIPT_DIR"
cd $SCRIPT_DIR
if [ -d $DIST_DIR/bin ]
then
rm -rf $DIST_DIR/bin
fi
if [ -d $DIST_DIR/share ]
then
rm -rf $DIST_DIR/share
fi
done
-51
View File
@@ -1,51 +0,0 @@
#!/bin/bash
set -e
source config.sh
for ARCH in $ARCHS
do
if [ -d dist-$ARCH ]
then
MAIN_ARCH=$ARCH
fi
done
if [ -z "$MAIN_ARCH" ]
then
echo "Please compile an architecture"
exit 1
fi
OUTPUT_DIR="dist-uarch"
rm -rf $OUTPUT_DIR
mkdir -p $OUTPUT_DIR/lib $OUTPUT_DIR/include
for LIB in dist-$MAIN_ARCH/lib/*.a
do
LIB=`basename $LIB`
LIPO_CREATE=""
for ARCH in $ARCHS
do
LIPO_ARCH=$ARCH
if [ "$ARCH" = "aarch64" ];
then
LIPO_ARCH="arm64"
fi
if [ -d dist-$ARCH ]
then
LIPO_CREATE="$LIPO_CREATE-arch $LIPO_ARCH dist-$ARCH/lib/$LIB "
fi
done
OUTPUT="$OUTPUT_DIR/lib/$LIB"
echo "Creating: $OUTPUT"
xcrun -sdk iphoneos lipo -create $LIPO_CREATE -output $OUTPUT
xcrun -sdk iphoneos lipo -info $OUTPUT
done
echo "Copying headers from dist-$MAIN_ARCH..."
cp -R dist-$MAIN_ARCH/include/* $OUTPUT_DIR/include
-36
View File
@@ -1,36 +0,0 @@
#!/bin/bash
OPUS_DIR="opus"
IOSSDK_VER=8.1
ARCHS="armv7 armv7s aarch64 i386 x86_64"
remove_arch() {
OLD_ARCHS="$ARCHS"
NEW_ARCHS=""
REMOVAL="$1"
for ARCH in $OLD_ARCHS; do
if [ "$ARCH" != "$REMOVAL" ] ; then
NEW_ARCHS="$NEW_ARCHS $ARCH"
fi
done
ARCHS=$NEW_ARCHS
}
# armv7s is only supported in iOS 6.0+
CHECK=`echo $IOSSDK_VER '>= 6.0' | bc -l`
if [ "$CHECK" = "0" ] ; then
remove_arch "armv7s"
fi
# armv6 is not supported any more since iOS 6.0
CHECK=`echo $IOSSDK_VER '< 6.0' | bc -l`
if [ "$CHECK" = "0" ] ; then
remove_arch "armv6"
fi
echo 'Architectures to build:' $ARCHS
-20
View File
@@ -1,20 +0,0 @@
#!/bin/sh
set -e
source config.sh
SCRIPT_DIR=$( (cd -P $(dirname $0) && pwd) )
DIST_DIR_BASE=${DIST_DIR_BASE:="$SCRIPT_DIR/dist"}
cd opus
git checkout master
git pull origin master
cd ..
for ARCH in $ARCHS
do
OPUS_DIR=opus-$ARCH
echo "Syncing source for $ARCH to directory $OPUS_DIR"
rsync opus/ $OPUS_DIR/ --exclude '.*' -a --delete
done
-104
View File
@@ -1,104 +0,0 @@
#!/bin/bash
SYSROOT_iPHONE="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS.sdk"
SYSROOT_SIMULATOR="/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneSimulator.platform/Developer/SDKs/iPhoneSimulator.sdk"
CC_IPHONE="xcrun -sdk iphoneos clang"
CC_SIMULATOR="xcrun -sdk iphonesimulator clang"
function build_one {
./configure \
--prefix=$PREFIX \
--disable-ffmpeg \
--disable-ffplay \
--disable-ffprobe \
--disable-ffserver \
--disable-armv5te \
--disable-armv6 \
--disable-doc \
--disable-everything \
--disable-debug \
--enable-decoder=h264 \
--enable-avresample \
--enable-cross-compile \
--sysroot=$SYSROOT \
--target-os=darwin \
--cc="$CC" \
--extra-cflags="$CFLAGS" \
--extra-ldflags="$LDFLAGS" \
--enable-pic \
$ADDI_FLAGS
make clean && make -j4 && make install
}
# armv7
function build_armv7 {
PREFIX="armv7"
SYSROOT=$SYSROOT_iPHONE
CC=$CC_IPHONE
CFLAGS="-arch armv7 -mfpu=neon -miphoneos-version-min=7.1 -fpic"
LDFLAGS="-arch armv7 -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
build_one
}
# armv7s
function build_armv7s {
PREFIX="armv7s"
SYSROOT=$SYSROOT_iPHONE
CC=$CC_IPHONE
CFLAGS="-arch armv7s -mfpu=neon -miphoneos-version-min=7.1"
LDFLAGS="-arch armv7s -isysroot $SYSROOT_iPHONE -miphoneos-version-min=7.1"
ADDI_FLAGS="--arch=arm --cpu=cortex-a9"
build_one
}
# i386
function build_i386 {
PREFIX="i386"
SYSROOT=$SYSROOT_SIMULATOR
CC=$CC_SIMULATOR
CFLAGS="-arch i386"
LDFLAGS="-arch i386 -isysroot $SYSROOT_SIMULATOR -mios-simulator-version-min=7.1"
ADDI_FLAGS="--arch=i386 --cpu=i386 --disable-asm"
build_one
}
# create fat library
function build_universal {
cd armv7/lib
for file in *.a
do
cd ../..
xcrun -sdk iphoneos lipo -output universal/lib/$file -create \
-arch armv7 armv7/lib/$file \
-arch armv7s armv7s/lib/$file \
-arch i386 i386/lib/$file
echo "Universal $file created."
cd -
done
cd ../..
}
echo "FFmpeg directory?"
read SOURCE
cd $SOURCE
if [ "$1" = "clean" ]
then
rm -r armv7 armv7s i386 universal
make clean
exit 0
fi
mkdir armv7
mkdir armv7s
mkdir i386
mkdir -p universal/lib
build_armv7
build_armv7s
build_i386
build_universal
echo "Ouput files in $SOURCE/armv7 $SOURCE/armv7s $SOURCE/i386 $SOURCE/universal"
+180
View File
@@ -0,0 +1,180 @@
#!/bin/bash
# Builds libopus for iOS or tvOS.
#
# Copyright 2012 Mike Tigas <mike@tig.as>
#
# Based on work by Felix Schulze on 16.12.10.
# Copyright 2010 Felix Schulze. All rights reserved.
#
# 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.
#
###########################################################################
# Choose your libopus version and your currently-installed iOS SDK version:
#
TARGET="AppleTV" # "iPhone"
VERSION="1.3.1"
SDKVERSION="16.1"
MINVERSIONDEF="-mtvos-version-min=12.0" # "-miphoneos-version-min=12.0"
###########################################################################
#
# Don't change anything under this line!
#
###########################################################################
# by default, we won't build for debugging purposes
if [ "${DEBUG}" == "true" ]; then
echo "Compiling for debugging ..."
OPT_CFLAGS="-O0 -fno-inline -g"
OPT_LDFLAGS=""
OPT_CONFIG_ARGS="--enable-assertions --disable-asm"
else
OPT_CFLAGS="-O3 -g"
OPT_LDFLAGS=""
OPT_CONFIG_ARGS=""
fi
# No need to change this since xcode build will only compile in the
# necessary bits from the libraries we create
ARCHS="x86_64 arm64"
DEVELOPER=`xcode-select -print-path`
#DEVELOPER="/Applications/Xcode.app/Contents/Developer"
cd "`dirname \"$0\"`"
REPOROOT=$(pwd)
# Where we'll end up storing things in the end
OUTPUTDIR="${REPOROOT}/dependencies"
mkdir -p ${OUTPUTDIR}/include
mkdir -p ${OUTPUTDIR}/lib
BUILDDIR="${REPOROOT}/build"
# where we will keep our sources and build from.
SRCDIR="${BUILDDIR}/src"
mkdir -p $SRCDIR
# where we will store intermediary builds
INTERDIR="${BUILDDIR}/built"
mkdir -p $INTERDIR
########################################
cd $SRCDIR
# Exit the script if an error happens
set -e
if [ ! -e "${SRCDIR}/opus-${VERSION}.tar.gz" ]; then
echo "Downloading opus-${VERSION}.tar.gz"
curl -LO http://downloads.xiph.org/releases/opus/opus-${VERSION}.tar.gz
fi
echo "Using opus-${VERSION}.tar.gz"
tar zxf opus-${VERSION}.tar.gz -C $SRCDIR
cd "${SRCDIR}/opus-${VERSION}"
set +e # don't bail out of bash script if ccache doesn't exist
CCACHE=`which ccache`
if [ $? == "0" ]; then
echo "Building with ccache: $CCACHE"
CCACHE="${CCACHE} "
else
echo "Building without ccache"
CCACHE=""
fi
set -e # back to regular "bail out on error" mode
export ORIGINALPATH=$PATH
for ARCH in ${ARCHS}
do
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ]; then
PLATFORM="${TARGET}Simulator"
EXTRA_CFLAGS="-arch ${ARCH}"
EXTRA_CONFIG="--host=x86_64-apple-darwin"
else
PLATFORM="${TARGET}OS"
EXTRA_CFLAGS="-arch ${ARCH}"
EXTRA_CONFIG="--host=arm-apple-darwin"
fi
mkdir -p "${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk"
./configure --disable-shared --enable-static --with-pic --disable-extra-programs --disable-doc ${EXTRA_CONFIG} \
--prefix="${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk" \
LDFLAGS="$LDFLAGS ${OPT_LDFLAGS} -fPIE ${MINVERSIONDEF} -L${OUTPUTDIR}/lib" \
CFLAGS="$CFLAGS ${EXTRA_CFLAGS} ${OPT_CFLAGS} -fPIE ${MINVERSIONDEF} -I${OUTPUTDIR}/include -isysroot ${DEVELOPER}/Platforms/${PLATFORM}.platform/Developer/SDKs/${PLATFORM}${SDKVERSION}.sdk" \
# Build the application and install it to the fake SDK intermediary dir
# we have set up. Make sure to clean up afterward because we will re-use
# this source tree to cross-compile other targets.
make -j$(nproc)
make install
make clean
done
########################################
echo "Build library..."
# These are the libs that comprise libopus.
OUTPUT_LIBS="libopus.a"
for OUTPUT_LIB in ${OUTPUT_LIBS}; do
INPUT_LIBS=""
for ARCH in ${ARCHS}; do
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ];
then
PLATFORM="${TARGET}Simulator"
else
PLATFORM="${TARGET}OS"
fi
INPUT_ARCH_LIB="${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk/lib/${OUTPUT_LIB}"
if [ -e $INPUT_ARCH_LIB ]; then
INPUT_LIBS="${INPUT_LIBS} ${INPUT_ARCH_LIB}"
fi
done
# Combine the three architectures into a universal library.
if [ -n "$INPUT_LIBS" ]; then
lipo -create $INPUT_LIBS \
-output "${OUTPUTDIR}/lib/${OUTPUT_LIB}"
else
echo "$OUTPUT_LIB does not exist, skipping (are the dependencies installed?)"
fi
done
for ARCH in ${ARCHS}; do
if [ "${ARCH}" == "i386" ] || [ "${ARCH}" == "x86_64" ];
then
PLATFORM="${TARGET}Simulator"
else
PLATFORM="${TARGET}OS"
fi
cp -R ${INTERDIR}/${PLATFORM}${SDKVERSION}-${ARCH}.sdk/include/* ${OUTPUTDIR}/include/
if [ $? == "0" ]; then
# We only need to copy the headers over once. (So break out of forloop
# once we get first success.)
break
fi
done
####################
echo "Building done."
echo "Cleaning up..."
rm -fr ${INTERDIR}
rm -fr "${SRCDIR}/opus-${VERSION}"
echo "Done."
-124
View File
@@ -1,124 +0,0 @@
#!/bin/bash
# Yay shell scripting! This script builds a static version of
# OpenSSL ${OPENSSL_VERSION} for iOS 7.0 that contains code for
# armv6, armv7, arm7s and i386.
#set -x
# Setup paths to stuff we need
OPENSSL_VERSION="1.0.1h"
DEVELOPER="/Applications/Xcode.app/Contents/Developer"
SDK_VERSION="7.1"
MIN_VERSION="7.1"
IPHONEOS_PLATFORM="${DEVELOPER}/Platforms/iPhoneOS.platform"
IPHONEOS_SDK="${IPHONEOS_PLATFORM}/Developer/SDKs/iPhoneOS.sdk"
IPHONEOS_GCC="/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang"
IPHONESIMULATOR_PLATFORM="${DEVELOPER}/Platforms/iPhoneSimulator.platform"
IPHONESIMULATOR_SDK="${IPHONESIMULATOR_PLATFORM}/Developer/SDKs/iPhoneSimulator.sdk"
IPHONESIMULATOR_GCC="/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/clang"
# Make sure things actually exist
if [ ! -d "$IPHONEOS_PLATFORM" ]; then
echo "Cannot find $IPHONEOS_PLATFORM"
exit 1
fi
if [ ! -d "$IPHONEOS_SDK" ]; then
echo "Cannot find $IPHONEOS_SDK"
exit 1
fi
if [ ! -x "$IPHONEOS_GCC" ]; then
echo "Cannot find $IPHONEOS_GCC"
exit 1
fi
if [ ! -d "$IPHONESIMULATOR_PLATFORM" ]; then
echo "Cannot find $IPHONESIMULATOR_PLATFORM"
exit 1
fi
if [ ! -d "$IPHONESIMULATOR_SDK" ]; then
echo "Cannot find $IPHONESIMULATOR_SDK"
exit 1
fi
if [ ! -x "$IPHONESIMULATOR_GCC" ]; then
echo "Cannot find $IPHONESIMULATOR_GCC"
exit 1
fi
# Clean up whatever was left from our previous build
rm -rf include lib
rm -rf /tmp/openssl-${OPENSSL_VERSION}-*
rm -rf /tmp/openssl-${OPENSSL_VERSION}-*.*-log
build()
{
TARGET=$1
ARCH=$2
GCC=$3
SDK=$4
EXTRA=$5
rm -rf "openssl-${OPENSSL_VERSION}"
tar xfz "openssl-${OPENSSL_VERSION}.tar.gz"
pushd .
cd "openssl-${OPENSSL_VERSION}"
./Configure ${TARGET} --openssldir="/tmp/openssl-${OPENSSL_VERSION}-${ARCH}" ${EXTRA} &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.log"
perl -i -pe 's|static volatile sig_atomic_t intr_signal|static volatile int intr_signal|' crypto/ui/ui_openssl.c
perl -i -pe "s|^CC= gcc|CC= ${GCC} -arch ${ARCH} -miphoneos-version-min=${MIN_VERSION}|g" Makefile
perl -i -pe "s|^CFLAG= (.*)|CFLAG= -isysroot ${SDK} \$1|g" Makefile
make &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.build-log"
make install &> "/tmp/openssl-${OPENSSL_VERSION}-${ARCH}.install-log"
popd
rm -rf "openssl-${OPENSSL_VERSION}"
}
mkdir openssl
cd openssl
if [ ! -e ${OPENSSL_VERSION}.tar.gz ]; then
echo "Downloading ${OPENSSL_VERSION}.tar.gz"
curl -O http://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
else
echo "Using ${OPENSSL_VERSION}.tar.gz"
fi
build "BSD-generic32" "armv7" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
build "BSD-generic32" "armv7s" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
build "BSD-generic64" "arm64" "${IPHONEOS_GCC}" "${IPHONEOS_SDK}" ""
build "BSD-generic32" "i386" "${IPHONESIMULATOR_GCC}" "${IPHONESIMULATOR_SDK}" ""
build "BSD-generic64" "x86_64" "${IPHONESIMULATOR_GCC}" "${IPHONESIMULATOR_SDK}" "-DOPENSSL_NO_ASM"
#
mkdir include
cp -r /tmp/openssl-${OPENSSL_VERSION}-i386/include/openssl include/
mkdir lib
lipo \
"/tmp/openssl-${OPENSSL_VERSION}-armv7/lib/libcrypto.a" \
"/tmp/openssl-${OPENSSL_VERSION}-armv7s/lib/libcrypto.a" \
"/tmp/openssl-${OPENSSL_VERSION}-arm64/lib/libcrypto.a" \
"/tmp/openssl-${OPENSSL_VERSION}-i386/lib/libcrypto.a" \
"/tmp/openssl-${OPENSSL_VERSION}-x86_64/lib/libcrypto.a" \
-create -output lib/libcrypto.a
lipo \
"/tmp/openssl-${OPENSSL_VERSION}-armv7/lib/libssl.a" \
"/tmp/openssl-${OPENSSL_VERSION}-armv7s/lib/libssl.a" \
"/tmp/openssl-${OPENSSL_VERSION}-arm64/lib/libssl.a" \
"/tmp/openssl-${OPENSSL_VERSION}-i386/lib/libssl.a" \
"/tmp/openssl-${OPENSSL_VERSION}-x86_64/lib/libssl.a" \
-create -output lib/libssl.a
rm -rf "/tmp/openssl-${OPENSSL_VERSION}-*"
rm -rf "/tmp/openssl-${OPENSSL_VERSION}-*.*-log"
+8
View File
@@ -672,3 +672,11 @@ may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
App Store Distribution Exception
As additional permission under section 7, you are allowed to distribute the
software through an app store, even if that store has restrictive terms and
conditions that are incompatible with the GPL, provided that the source is
also available under the GPL with or without this permission through a channel
without those restrictive terms and conditions.
+2
View File
@@ -96,6 +96,7 @@ static NSData* p12 = nil;
bio = BIO_new(BIO_s_mem());
i2d_X509_bio(bio, x509);
X509_free(x509);
BUF_MEM* mem;
BIO_get_mem_ptr(bio, &mem);
@@ -222,6 +223,7 @@ static NSData* p12 = nil;
+ (NSData *)getSignatureFromCert:(NSData *)cert {
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
BIO_free(bio);
if (!x509) {
Log(LOG_E, @"Unable to parse certificate in memory!");
+19 -9
View File
@@ -4,10 +4,12 @@
#include <stdio.h>
#include <stdlib.h>
#include <openssl/evp.h>
#include <openssl/pem.h>
#include <openssl/rsa.h>
#include <OpenSSL/provider.h>
#include <OpenSSL/rsa.h>
#include <openssl/x509.h>
#include <openssl/rand.h>
#include <OpenSSL/rand.h>
static const int NUM_BITS = 2048;
static const int SERIAL = 0;
@@ -61,22 +63,30 @@ void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
}
struct CertKeyPair generateCertKeyPair(void) {
BIO *bio_err;
X509 *x509 = NULL;
EVP_PKEY *pkey = NULL;
PKCS12 *p12 = NULL;
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
char* pass = "limelight";
p12 = PKCS12_create(pass,
"GameStream",
pkey,
x509,
NULL,
NID_pbe_WithSHA1And3_Key_TripleDES_CBC,
-1, // disable certificate encryption
2048,
-1, // disable the automatic MAC
0);
// MAC it ourselves with SHA1 since iOS refuses to load anything else.
PKCS12_set_mac(p12, pass, -1, NULL, 0, 1, EVP_sha1());
if (p12 == NULL) {
printf("Error generating a valid PKCS12 certificate.\n");
}
BIO_free(bio_err);
return (CertKeyPair){x509, pkey, p12};
}
+4 -1
View File
@@ -17,11 +17,14 @@
framerate:(NSInteger)framerate
height:(NSInteger)height
width:(NSInteger)width
audioConfig:(NSInteger)audioConfig
onscreenControls:(NSInteger)onscreenControls
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
useHevc:(BOOL)useHevc
preferredCodec:(uint32_t)preferredCodec
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
+8 -2
View File
@@ -56,11 +56,14 @@
framerate:(NSInteger)framerate
height:(NSInteger)height
width:(NSInteger)width
audioConfig:(NSInteger)audioConfig
onscreenControls:(NSInteger)onscreenControls
optimizeGames:(BOOL)optimizeGames
multiController:(BOOL)multiController
swapABXYButtons:(BOOL)swapABXYButtons
audioOnPC:(BOOL)audioOnPC
useHevc:(BOOL)useHevc
preferredCodec:(uint32_t)preferredCodec
useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
@@ -72,11 +75,14 @@
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
settingsToSave.height = [NSNumber numberWithInteger:height];
settingsToSave.width = [NSNumber numberWithInteger:width];
settingsToSave.audioConfig = [NSNumber numberWithInteger:audioConfig];
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
settingsToSave.optimizeGames = optimizeGames;
settingsToSave.multiController = multiController;
settingsToSave.swapABXYButtons = swapABXYButtons;
settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.useHevc = useHevc;
settingsToSave.preferredCodec = preferredCodec;
settingsToSave.useFramePacing = useFramePacing;
settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport;
settingsToSave.absoluteTouchMode = absoluteTouchMode;
+2
View File
@@ -15,6 +15,8 @@
@property (atomic) PairState pairState;
@property (atomic, nullable, retain) NSString * activeAddress;
@property (atomic, nullable, retain) NSString * currentGame;
@property (atomic) unsigned short httpsPort;
@property (atomic) BOOL isNvidiaServerSoftware;
@property (atomic, nullable, retain) NSData *serverCert;
@property (atomic, nullable, retain) NSString *address;
+5
View File
@@ -34,6 +34,11 @@
self.serverCodecModeSupport = host.serverCodecModeSupport;
self.serverCert = host.serverCert;
// Older clients stored a non-URL-escaped IPv6 string. Try to detect that and fix it up.
if (![self.ipv6Address containsString:@"["]) {
self.ipv6Address = [Utils addressAndPortToAddressPortString:self.ipv6Address port:47989];
}
// 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;
+9 -1
View File
@@ -16,10 +16,18 @@
@property (nonatomic, retain) NSNumber * framerate;
@property (nonatomic, retain) NSNumber * height;
@property (nonatomic, retain) NSNumber * width;
@property (nonatomic, retain) NSNumber * audioConfig;
@property (nonatomic, retain) NSNumber * onscreenControls;
@property (nonatomic, retain) NSString * uniqueId;
@property (nonatomic) BOOL useHevc;
@property (nonatomic) enum {
CODEC_PREF_AUTO,
CODEC_PREF_H264,
CODEC_PREF_HEVC,
CODEC_PREF_AV1,
} preferredCodec;
@property (nonatomic) BOOL useFramePacing;
@property (nonatomic) BOOL multiController;
@property (nonatomic) BOOL swapABXYButtons;
@property (nonatomic) BOOL playAudioOnPC;
@property (nonatomic) BOOL optimizeGames;
@property (nonatomic) BOOL enableHdr;
+13 -2
View File
@@ -34,11 +34,15 @@
assert([self.bitrate intValue] != 0);
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
assert([self.framerate intValue] != 0);
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"];
self.audioConfig = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"audioConfig"]];
assert([self.audioConfig intValue] != 0);
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
self.swapABXYButtons = [[NSUserDefaults standardUserDefaults] boolForKey:@"swapABXYButtons"];
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
@@ -56,6 +60,10 @@
self.height = [NSNumber numberWithInteger:2160];
self.width = [NSNumber numberWithInteger:3840];
break;
case 3:
self.height = [NSNumber numberWithInteger:1440];
self.width = [NSNumber numberWithInteger:2560];
break;
default:
abort();
}
@@ -65,11 +73,14 @@
self.framerate = settings.framerate;
self.height = settings.height;
self.width = settings.width;
self.useHevc = settings.useHevc;
self.audioConfig = settings.audioConfig;
self.preferredCodec = settings.preferredCodec;
self.useFramePacing = settings.useFramePacing;
self.playAudioOnPC = settings.playAudioOnPC;
self.enableHdr = settings.enableHdr;
self.optimizeGames = settings.optimizeGames;
self.multiController = settings.multiController;
self.swapABXYButtons = settings.swapABXYButtons;
self.onscreenControls = settings.onscreenControls;
self.btMouseSupport = settings.btMouseSupport;
self.absoluteTouchMode = settings.absoluteTouchMode;
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "AltIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"info" : {
"version" : 1,
"author" : "xcode"
"author" : "xcode",
"version" : 1
}
}
}
@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ControlIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "DeleteIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "doneIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 405 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "EscapeIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "ShiftIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "TabIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

@@ -0,0 +1,21 @@
{
"images" : [
{
"filename" : "WindowsIcon.png",
"idiom" : "universal",
"scale" : "1x"
},
{
"idiom" : "universal",
"scale" : "2x"
},
{
"idiom" : "universal",
"scale" : "3x"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

+22
View File
@@ -13,6 +13,11 @@
@interface Controller : NSObject
typedef struct {
float lastX;
float lastY;
} controller_touch_context_t;
@property (nullable, nonatomic, retain) GCController* gamepad;
@property (nonatomic) int playerIndex;
@property (nonatomic) int lastButtonFlags;
@@ -25,7 +30,24 @@
@property (nonatomic) short lastRightStickX;
@property (nonatomic) short lastRightStickY;
@property (nonatomic) controller_touch_context_t primaryTouch;
@property (nonatomic) controller_touch_context_t secondaryTouch;
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
@property (nonatomic) HapticContext* _Nullable leftTriggerMotor;
@property (nonatomic) HapticContext* _Nullable rightTriggerMotor;
@property (nonatomic) NSTimer* _Nullable accelTimer;
@property (nonatomic) GCAcceleration lastAccelSample;
@property (nonatomic) NSTimer* _Nullable gyroTimer;
@property (nonatomic) GCRotationRate lastGyroSample;
@property (nonatomic) NSTimer* _Nullable batteryTimer;
@property (nonatomic) GCDeviceBatteryState lastBatteryState;
@property (nonatomic) float lastBatteryLevel;
@property (nonatomic) BOOL reportedArrival;
@property (nonatomic) Controller* _Nullable mergedWithController;
@end
+7 -2
View File
@@ -11,16 +11,18 @@
@class OnScreenControls;
@protocol InputPresenceDelegate <NSObject>
@protocol ControllerSupportDelegate <NSObject>
- (void) gamepadPresenceChanged;
- (void) mousePresenceChanged;
- (void) streamExitRequested;
@end
@interface ControllerSupport : NSObject
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<InputPresenceDelegate>)delegate;
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) connectionEstablished;
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
-(void) cleanup;
@@ -40,6 +42,9 @@
-(void) updateFinished:(Controller*)controller;
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
-(void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
+575 -89
View File
@@ -17,7 +17,7 @@
@import GameController;
@import AudioToolbox;
static const double MOUSE_SPEED_DIVISOR = 2.5;
static const double MOUSE_SPEED_DIVISOR = 1.25;
@implementation ControllerSupport {
id _controllerConnectObserver;
@@ -29,17 +29,15 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
NSLock *_controllerStreamLock;
NSMutableDictionary *_controllers;
id<InputPresenceDelegate> _presenceDelegate;
id<ControllerSupportDelegate> _delegate;
float accumulatedDeltaX;
float accumulatedDeltaY;
float accumulatedScrollX;
float accumulatedScrollY;
OnScreenControls *_osc;
// This controller object is shared between on-screen controls
// and player 0
Controller *_player0osc;
Controller *_oscController;
#define EMULATING_SELECT 0x1
#define EMULATING_SPECIAL 0x2
@@ -47,18 +45,20 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
bool _oscEnabled;
char _controllerNumbers;
bool _multiController;
bool _swapABXYButtons;
}
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
#define UPDATE_BUTTON_FLAG(controller, x, y) \
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
#define MAX_MAGNITUDE(x, y) (abs(x) > abs(y) ? (x) : (y))
-(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;
// TODO: Rumble emulation for OSC
}
if (controller == nil) {
// No connected controller for this player
@@ -69,6 +69,126 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[controller.highFreqMotor setMotorAmplitude:highFreqMotor];
}
-(void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger
{
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
if (controller == nil && controllerNumber == 0 && _oscEnabled) {
// TODO: Trigger rumble emulation for OSC
}
if (controller == nil) {
// No connected controller for this player
return;
}
[controller.leftTriggerMotor setMotorAmplitude:leftTrigger];
[controller.rightTriggerMotor setMotorAmplitude:rightTrigger];
}
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz
{
if (@available(iOS 14.0, tvOS 14.0, *)) {
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
if (controller == nil) {
// No connected controller for this player
return;
}
if (controller.gamepad.motion == nil) {
// No motion supported for this controller
return;
}
switch (motionType) {
case LI_MOTION_TYPE_ACCEL:
[controller.accelTimer invalidate];
controller.accelTimer = nil;
if (reportRateHz && controller.gamepad.motion.hasGravityAndUserAcceleration) {
// Reset the last motion sample
GCAcceleration emptyAccelSample = {};
controller.lastAccelSample = emptyAccelSample;
dispatch_sync(dispatch_get_main_queue(), ^{
controller.accelTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / reportRateHz repeats:YES block:^(NSTimer *timer) {
// Don't send duplicate samples
GCAcceleration lastAccelSample = controller.lastAccelSample;
GCAcceleration accelSample = controller.gamepad.motion.acceleration;
if (memcmp(&accelSample, &lastAccelSample, sizeof(accelSample)) == 0) {
return;
}
controller.lastAccelSample = accelSample;
// Convert g to m/s^2
LiSendControllerMotionEvent((uint8_t)controllerNumber,
LI_MOTION_TYPE_ACCEL,
accelSample.x * -9.80665f,
accelSample.y * -9.80665f,
accelSample.z * -9.80665f);
}];
});
}
break;
case LI_MOTION_TYPE_GYRO:
[controller.gyroTimer invalidate];
controller.gyroTimer = nil;
if (reportRateHz && controller.gamepad.motion.hasRotationRate) {
// Reset the last motion sample
GCRotationRate emptyGyroSample = {};
controller.lastGyroSample = emptyGyroSample;
dispatch_sync(dispatch_get_main_queue(), ^{
controller.gyroTimer = [NSTimer scheduledTimerWithTimeInterval:1.0 / reportRateHz repeats:YES block:^(NSTimer *timer) {
// Don't send duplicate samples
GCRotationRate lastGyroSample = controller.lastGyroSample;
GCRotationRate gyroSample = controller.gamepad.motion.rotationRate;
if (memcmp(&gyroSample, &lastGyroSample, sizeof(gyroSample)) == 0) {
return;
}
controller.lastGyroSample = gyroSample;
// Convert rad/s to deg/s
LiSendControllerMotionEvent((uint8_t)controllerNumber,
LI_MOTION_TYPE_GYRO,
gyroSample.x * 57.2957795f,
gyroSample.z * 57.2957795f,
gyroSample.y * -57.2957795f);
}];
});
}
break;
}
// Set the motion sensor state if they require manual activation
if (controller.gamepad.motion.sensorsRequireManualActivation) {
if (controller.gyroTimer || controller.accelTimer) {
controller.gamepad.motion.sensorsActive = YES;
}
else {
controller.gamepad.motion.sensorsActive = NO;
}
}
}
}
-(void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
if (@available(iOS 14.0, tvOS 14.0, *)) {
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
if (controller == nil) {
// No connected controller for this player
return;
}
if (controller.gamepad.light == nil) {
// No LED control supported for this controller
return;
}
controller.gamepad.light.color = [[GCColor alloc] initWithRed:(r / 255.0f) green:(g / 255.0f) blue:(b / 255.0f)];
}
}
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
{
@synchronized(controller) {
@@ -196,15 +316,58 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
}
}
-(uint16_t) getActiveGamepadMask
{
return (_multiController ? _controllerNumbers : 1) | (_oscEnabled ? 1 : 0);
}
-(void) updateFinished:(Controller*)controller
{
BOOL exitRequested = NO;
[_controllerStreamLock lock];
@synchronized(controller) {
// Player 1 is always present for OSC
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);
// Handle Start+Select+L1+R1 gamepad quit combo
if (controller.lastButtonFlags == (PLAY_FLAG | BACK_FLAG | LB_FLAG | RB_FLAG)) {
controller.lastButtonFlags = 0;
exitRequested = YES;
}
// Only send controller events if we successfully reported controller arrival
if ([self reportControllerArrival:controller]) {
uint32_t buttonFlags = controller.lastButtonFlags;
uint8_t leftTrigger = controller.lastLeftTrigger;
uint8_t rightTrigger = controller.lastRightTrigger;
int16_t leftStickX = controller.lastLeftStickX;
int16_t leftStickY = controller.lastLeftStickY;
int16_t rightStickX = controller.lastRightStickX;
int16_t rightStickY = controller.lastRightStickY;
// If this is merged with another controller, combine the inputs
if (controller.mergedWithController) {
buttonFlags |= controller.mergedWithController.lastButtonFlags;
leftTrigger = MAX(leftTrigger, controller.mergedWithController.lastLeftTrigger);
rightTrigger = MAX(rightTrigger, controller.mergedWithController.lastRightTrigger);
leftStickX = MAX_MAGNITUDE(leftStickX, controller.mergedWithController.lastLeftStickX);
leftStickY = MAX_MAGNITUDE(leftStickY, controller.mergedWithController.lastLeftStickY);
rightStickX = MAX_MAGNITUDE(rightStickX, controller.mergedWithController.lastRightStickX);
rightStickY = MAX_MAGNITUDE(rightStickY, controller.mergedWithController.lastRightStickY);
}
// Player 1 is always present for OSC
LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0, [self getActiveGamepadMask],
buttonFlags, leftTrigger, rightTrigger,
leftStickX, leftStickY, rightStickX, rightStickY);
}
}
[_controllerStreamLock unlock];
if (exitRequested) {
// Invoke the delegate callback on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
[self->_delegate streamExitRequested];
});
}
}
+(BOOL) hasKeyboardOrMouse {
@@ -227,16 +390,13 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
if (controller.extendedGamepad != NULL) {
// Re-enable system gestures on the gamepad buttons now
if (@available(iOS 14.0, tvOS 14.0, *)) {
for (GCControllerElement* element in controller.extendedGamepad.allElements) {
for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
element.preferredSystemGestureState = GCSystemGestureStateEnabled;
}
}
controller.extendedGamepad.valueChangedHandler = NULL;
}
else if (controller.gamepad != NULL) {
controller.gamepad.valueChangedHandler = NULL;
}
}
}
@@ -244,12 +404,276 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
{
controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad];
controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad];
controller.leftTriggerMotor = [HapticContext createContextForLeftTrigger:controller.gamepad];
controller.rightTriggerMotor = [HapticContext createContextForRightTrigger:controller.gamepad];
}
-(void) cleanupControllerHaptics:(Controller*) controller
{
[controller.lowFreqMotor cleanup];
[controller.highFreqMotor cleanup];
[controller.leftTriggerMotor cleanup];
[controller.rightTriggerMotor cleanup];
}
-(void) cleanupControllerMotion:(Controller*) controller
{
if (@available(iOS 14.0, tvOS 14.0, *)) {
// Stop sensor sampling timers
[controller.gyroTimer invalidate];
[controller.accelTimer invalidate];
// Disable motion sensors if they require manual activation
if (controller.gamepad && controller.gamepad.motion && controller.gamepad.motion.sensorsRequireManualActivation) {
controller.gamepad.motion.sensorsActive = NO;
}
}
}
-(void) initializeControllerBattery:(Controller*) controller
{
if (@available(iOS 14.0, tvOS 14.0, *)) {
if (controller.gamepad.battery) {
// Poll for updated battery status every 30 seconds
controller.batteryTimer = [NSTimer scheduledTimerWithTimeInterval:30 repeats:YES block:^(NSTimer *timer) {
if (controller.lastBatteryState != controller.gamepad.battery.batteryState ||
controller.lastBatteryLevel != controller.gamepad.battery.batteryLevel) {
uint8_t batteryState;
switch (controller.gamepad.battery.batteryState) {
case GCDeviceBatteryStateFull:
batteryState = LI_BATTERY_STATE_FULL;
break;
case GCDeviceBatteryStateCharging:
batteryState = LI_BATTERY_STATE_CHARGING;
break;
case GCDeviceBatteryStateDischarging:
batteryState = LI_BATTERY_STATE_DISCHARGING;
break;
case GCDeviceBatteryStateUnknown:
default:
batteryState = LI_BATTERY_STATE_UNKNOWN;
break;
}
LiSendControllerBatteryEvent(controller.playerIndex, batteryState, (uint8_t)(controller.gamepad.battery.batteryLevel * 100));
controller.lastBatteryState = controller.gamepad.battery.batteryState;
controller.lastBatteryLevel = controller.gamepad.battery.batteryLevel;
}
}];
// Fire the timer immediately to send the initial battery state
[controller.batteryTimer fire];
}
}
}
-(void) cleanupControllerBattery:(Controller*) controller
{
if (@available(iOS 14.0, tvOS 14.0, *)) {
[controller.batteryTimer invalidate];
}
}
-(BOOL) reportControllerArrival:(Controller*) limeController
{
// Only report arrival once
if (limeController.reportedArrival) {
return YES;
}
uint8_t type = LI_CTYPE_UNKNOWN;
uint16_t capabilities = 0;
uint32_t supportedButtonFlags = 0;
GCController *controller = limeController.gamepad;
if (controller) {
// This is a physical controller with a corresponding GCController object
// Start is always present
supportedButtonFlags |= PLAY_FLAG;
// Detect buttons present in the GCExtendedGamepad profile
if (controller.extendedGamepad.dpad) {
supportedButtonFlags |= UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG;
}
if (controller.extendedGamepad.leftShoulder) {
supportedButtonFlags |= LB_FLAG;
}
if (controller.extendedGamepad.rightShoulder) {
supportedButtonFlags |= RB_FLAG;
}
if (@available(iOS 13.0, tvOS 13.0, *)) {
if (controller.extendedGamepad.buttonOptions) {
supportedButtonFlags |= BACK_FLAG;
}
}
if (@available(iOS 14.0, tvOS 14.0, *)) {
if (controller.extendedGamepad.buttonHome) {
supportedButtonFlags |= SPECIAL_FLAG;
}
}
if (controller.extendedGamepad.buttonA) {
supportedButtonFlags |= A_FLAG;
}
if (controller.extendedGamepad.buttonB) {
supportedButtonFlags |= B_FLAG;
}
if (controller.extendedGamepad.buttonX) {
supportedButtonFlags |= X_FLAG;
}
if (controller.extendedGamepad.buttonY) {
supportedButtonFlags |= Y_FLAG;
}
if (@available(iOS 12.1, tvOS 12.1, *)) {
if (controller.extendedGamepad.leftThumbstickButton) {
supportedButtonFlags |= LS_CLK_FLAG;
}
if (controller.extendedGamepad.rightThumbstickButton) {
supportedButtonFlags |= RS_CLK_FLAG;
}
}
if (@available(iOS 14.0, tvOS 14.0, *)) {
// Xbox One/Series controller
if (controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) {
supportedButtonFlags |= PADDLE1_FLAG;
}
if (controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) {
supportedButtonFlags |= PADDLE2_FLAG;
}
if (controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) {
supportedButtonFlags |= PADDLE3_FLAG;
}
if (controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) {
supportedButtonFlags |= PADDLE4_FLAG;
}
if (@available(iOS 15.0, tvOS 15.0, *)) {
if (controller.physicalInputProfile.buttons[GCInputButtonShare]) {
supportedButtonFlags |= MISC_FLAG;
}
}
// DualShock/DualSense controller
if (controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) {
supportedButtonFlags |= TOUCHPAD_FLAG;
}
if (controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
capabilities |= LI_CCAP_TOUCHPAD;
}
if ([controller.extendedGamepad isKindOfClass:[GCXboxGamepad class]]) {
type = LI_CTYPE_XBOX;
}
else if ([controller.extendedGamepad isKindOfClass:[GCDualShockGamepad class]]) {
type = LI_CTYPE_PS;
}
if (@available(iOS 14.5, tvOS 14.5, *)) {
if ([controller.extendedGamepad isKindOfClass:[GCDualSenseGamepad class]]) {
type = LI_CTYPE_PS;
}
}
// Detect supported haptics localities
if (controller.haptics) {
if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityHandles]) {
capabilities |= LI_CCAP_RUMBLE;
}
if ([controller.haptics.supportedLocalities containsObject:GCHapticsLocalityTriggers]) {
capabilities |= LI_CCAP_TRIGGER_RUMBLE;
}
}
// Detect supported motion sensors
if (controller.motion) {
if (controller.motion.hasGravityAndUserAcceleration) {
capabilities |= LI_CCAP_ACCEL;
}
if (controller.motion.hasRotationRate) {
capabilities |= LI_CCAP_GYRO;
}
}
// Detect RGB LED support
if (controller.light) {
capabilities |= LI_CCAP_RGB_LED;
}
// Detect battery support
if (controller.battery) {
capabilities |= LI_CCAP_BATTERY_STATE;
}
}
else {
// This is a virtual controller corresponding to our OSC
// TODO: Support various layouts and button labels on the OSC
type = LI_CTYPE_XBOX;
capabilities = 0;
supportedButtonFlags =
PLAY_FLAG | BACK_FLAG | UP_FLAG | DOWN_FLAG | LEFT_FLAG | RIGHT_FLAG |
LB_FLAG | RB_FLAG | LS_CLK_FLAG | RS_CLK_FLAG | A_FLAG | B_FLAG | X_FLAG | Y_FLAG;
}
}
// Report the new controller to the host
// NB: This will fail if the connection hasn't been fully established yet
// and we will try again later.
if (LiSendControllerArrivalEvent(controller.playerIndex,
[self getActiveGamepadMask],
type,
supportedButtonFlags,
capabilities) != 0) {
return NO;
}
// Begin polling for battery status
[self initializeControllerBattery:limeController];
// Remember that we've reported arrival already
limeController.reportedArrival = YES;
return YES;
}
-(void) handleControllerTouchpad:(Controller*)controller touch:(GCControllerDirectionPad*)touch index:(int)index
{
controller_touch_context_t context = index == 0 ? controller.primaryTouch : controller.secondaryTouch;
// This magic is courtesy of SDL
float normalizedX = (1.0f + touch.xAxis.value) * 0.5f;
float normalizedY = 1.0f - (1.0f + touch.yAxis.value) * 0.5f;
// If we went from a touch to no touch, generate a touch up event
if ((context.lastX || context.lastY) && (!touch.xAxis.value && !touch.yAxis.value)) {
LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_UP, index, normalizedX, normalizedY, 1.0f);
}
else if (touch.xAxis.value || touch.yAxis.value) {
// If we went from no touch to a touch, generate a touch down event
if (!context.lastX && !context.lastY) {
LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_DOWN, index, normalizedX, normalizedY, 1.0f);
}
else if (context.lastX != touch.xAxis.value || context.lastY != touch.yAxis.value) {
// Otherwise it's just a move
LiSendControllerTouchEvent(controller.playerIndex, LI_TOUCH_EVENT_MOVE, index, normalizedX, normalizedY, 1.0f);
}
}
// We have to assign the whole struct because this is a property rather than a standard
// field that we could modify through a pointer.
if (index == 0) {
controller.primaryTouch = (controller_touch_context_t) {
touch.xAxis.value,
touch.yAxis.value
};
}
else {
controller.secondaryTouch = (controller_touch_context_t) {
touch.xAxis.value,
touch.yAxis.value
};
}
}
-(void) registerControllerCallbacks:(GCController*) controller
@@ -291,7 +715,7 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
// Disable system gestures on the gamepad to avoid interfering
// with in-game controller actions
if (@available(iOS 14.0, tvOS 14.0, *)) {
for (GCControllerElement* element in controller.extendedGamepad.allElements) {
for (GCControllerElement* element in controller.physicalInputProfile.allElements) {
element.preferredSystemGestureState = GCSystemGestureStateDisabled;
}
}
@@ -302,10 +726,18 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
short rightStickX, rightStickY;
unsigned char leftTrigger, rightTrigger;
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);
UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
if (self->_swapABXYButtons) {
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonA.pressed);
UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonB.pressed);
UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonX.pressed);
UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonY.pressed);
}
else {
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);
UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
}
UPDATE_BUTTON_FLAG(limeController, UP_FLAG, gamepad.dpad.up.pressed);
UPDATE_BUTTON_FLAG(limeController, DOWN_FLAG, gamepad.dpad.down.pressed);
@@ -341,6 +773,40 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
if (gamepad.buttonHome != nil) {
UPDATE_BUTTON_FLAG(limeController, SPECIAL_FLAG, gamepad.buttonHome.pressed);
}
// Xbox One/Series controllers
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne]) {
UPDATE_BUTTON_FLAG(limeController, PADDLE1_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleOne].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo]) {
UPDATE_BUTTON_FLAG(limeController, PADDLE2_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleTwo].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree]) {
UPDATE_BUTTON_FLAG(limeController, PADDLE3_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleThree].pressed);
}
if (gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour]) {
UPDATE_BUTTON_FLAG(limeController, PADDLE4_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputXboxPaddleFour].pressed);
}
if (@available(iOS 15.0, tvOS 15.0, *)) {
if (gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare]) {
UPDATE_BUTTON_FLAG(limeController, MISC_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputButtonShare].pressed);
}
}
// DualShock/DualSense controllers
if (gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton]) {
UPDATE_BUTTON_FLAG(limeController, TOUCHPAD_FLAG, gamepad.controller.physicalInputProfile.buttons[GCInputDualShockTouchpadButton].pressed);
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]) {
[self handleControllerTouchpad:limeController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadOne]
index:0];
}
if (gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]) {
[self handleControllerTouchpad:limeController
touch:gamepad.controller.physicalInputProfile.dpads[GCInputDualShockTouchpadTwo]
index:1];
}
}
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
@@ -358,25 +824,6 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[self updateFinished:limeController];
};
}
else if (controller.gamepad != NULL) {
controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
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);
UPDATE_BUTTON_FLAG(limeController, Y_FLAG, gamepad.buttonY.pressed);
UPDATE_BUTTON_FLAG(limeController, UP_FLAG, gamepad.dpad.up.pressed);
UPDATE_BUTTON_FLAG(limeController, DOWN_FLAG, gamepad.dpad.down.pressed);
UPDATE_BUTTON_FLAG(limeController, LEFT_FLAG, gamepad.dpad.left.pressed);
UPDATE_BUTTON_FLAG(limeController, RIGHT_FLAG, gamepad.dpad.right.pressed);
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
[self updateFinished:limeController];
};
}
} else {
Log(LOG_W, @"Tried to register controller callbacks on NULL controller");
}
@@ -392,6 +839,11 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
for (GCControllerButtonInput* auxButton in mouse.mouseInput.auxiliaryButtons) {
auxButton.pressedChangedHandler = nil;
}
#if TARGET_OS_TV
mouse.mouseInput.scroll.xAxis.valueChangedHandler = nil;
mouse.mouseInput.scroll.yAxis.valueChangedHandler = nil;
#endif
}
-(void) registerMouseCallbacks:(GCMouse*) mouse API_AVAILABLE(ios(14.0)) {
@@ -438,6 +890,18 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
// in iPadOS 15 where discrete scroll events are dropped. tvOS only supports
// GCMouse for mice, so we will have to just use it and hope for the best.
#if TARGET_OS_TV
mouse.mouseInput.scroll.xAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
self->accumulatedScrollX += value;
short truncatedScrollX = (short)self->accumulatedScrollX;
if (truncatedScrollX != 0) {
// Direction is reversed from vertical scrolling
LiSendHighResHScrollEvent(-truncatedScrollX * 20);
self->accumulatedScrollX -= truncatedScrollX;
}
};
mouse.mouseInput.scroll.yAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
self->accumulatedScrollY += value;
@@ -483,10 +947,6 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
}
break;
}
else if (controller.gamepad != NULL) {
level = OnScreenControlsLevelAutoGCGamepad;
break;
}
}
}
@@ -511,24 +971,20 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[self updateAutoOnScreenControlMode];
}
-(void) assignController:(GCController*)controller {
-(Controller*) assignController:(GCController*)controller {
for (int i = 0; i < 4; i++) {
if (!(_controllerNumbers & (1 << i))) {
_controllerNumbers |= (1 << i);
controller.playerIndex = i;
Controller* limeController;
if (i == 0) {
// Player 0 shares a controller object with the on-screen controls
limeController = _player0osc;
} else {
limeController = [[Controller alloc] init];
limeController.playerIndex = i;
}
Controller* limeController = [[Controller alloc] init];
limeController.playerIndex = i;
limeController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
limeController.gamepad = controller;
// If this is player 0, it shares state with the OSC
limeController.mergedWithController = _oscController;
_oscController.mergedWithController = limeController;
if (@available(iOS 13.0, tvOS 13.0, *)) {
if (controller.extendedGamepad != nil &&
@@ -552,17 +1008,19 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
Log(LOG_I, @"Assigning controller index: %d", i);
break;
return limeController;
}
}
return nil;
}
-(Controller*) getOscController {
return _player0osc;
return _oscController;
}
+(bool) isSupportedGamepad:(GCController*) controller {
return controller.extendedGamepad != nil || controller.gamepad != nil;
return controller.extendedGamepad != nil;
}
#pragma clang diagnostic pop
@@ -615,7 +1073,7 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
return _controllers.count;
}
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<InputPresenceDelegate>)delegate
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate
{
self = [super init];
@@ -623,10 +1081,11 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
_controllers = [[NSMutableDictionary alloc] init];
_controllerNumbers = 0;
_multiController = streamConfig.multiController;
_presenceDelegate = delegate;
_swapABXYButtons = streamConfig.swapABXYButtons;
_delegate = delegate;
_player0osc = [[Controller alloc] init];
_player0osc.playerIndex = 0;
_oscController = [[Controller alloc] init];
_oscController.playerIndex = 0;
DataManager* dataMan = [[DataManager alloc] init];
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
@@ -638,6 +1097,9 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
if ([ControllerSupport isSupportedGamepad:controller]) {
[self assignController:controller];
[self registerControllerCallbacks:controller];
// Note: We cannot report controller arrival to the host here,
// because the connection has not been established yet.
}
}
@@ -657,16 +1119,20 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
return;
}
[self assignController:controller];
// Register callbacks on the new controller
[self registerControllerCallbacks:controller];
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_presenceDelegate gamepadPresenceChanged];
Controller* limeController = [self assignController:controller];
if (limeController) {
// Register callbacks on the new controller
[self registerControllerCallbacks:controller];
// Report the controller arrival to the host if we're connected
[self reportControllerArrival:limeController];
// Re-evaluate the on-screen control mode
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_delegate gamepadPresenceChanged];
}
}];
_controllerDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Controller disconnected!");
@@ -682,23 +1148,33 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
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]];
// Stop haptics on this controller
[self cleanupControllerHaptics:limeController];
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];
// Notify the delegate
[self->_presenceDelegate gamepadPresenceChanged];
if (limeController) {
// Stop haptics on this controller
[self cleanupControllerHaptics:limeController];
// Stop motion reports on this controller
[self cleanupControllerMotion:limeController];
// Stop battery reports on this controller
[self cleanupControllerBattery:limeController];
// Disassociate this controller from any controllers merged with it
if (limeController.mergedWithController) {
assert(limeController.mergedWithController.mergedWithController == limeController);
limeController.mergedWithController.mergedWithController = 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];
// Notify the delegate
[self->_delegate gamepadPresenceChanged];
}
}];
if (@available(iOS 14.0, tvOS 14.0, *)) {
@@ -714,7 +1190,7 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_presenceDelegate mousePresenceChanged];
[self->_delegate mousePresenceChanged];
}];
_mouseDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Mouse disconnected!");
@@ -728,7 +1204,7 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
[self updateAutoOnScreenControlMode];
// Notify the delegate
[self->_presenceDelegate mousePresenceChanged];
[self->_delegate mousePresenceChanged];
}];
_keyboardConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
Log(LOG_I, @"Keyboard connected!");
@@ -747,6 +1223,14 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
return self;
}
-(void) connectionEstablished
{
for (Controller* controller in [_controllers allValues]) {
// Report the controller arrival to the host if we haven't done so yet
[self reportControllerArrival:controller];
}
}
-(void) cleanup
{
[[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver];
@@ -767,6 +1251,8 @@ static const double MOUSE_SPEED_DIVISOR = 2.5;
for (Controller* controller in [_controllers allValues]) {
[self cleanupControllerHaptics:controller];
[self cleanupControllerMotion:controller];
[self cleanupControllerBattery:controller];
}
[_controllers removeAllObjects];
+2
View File
@@ -16,5 +16,7 @@
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad;
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad;
@end
+23
View File
@@ -89,6 +89,11 @@
return nil;
}
if (![[gamepad.haptics supportedLocalities] containsObject:locality]) {
Log(LOG_W, @"Controller %d does not support haptic locality: %@", gamepad.playerIndex, locality);
return nil;
}
_playerIndex = gamepad.playerIndex;
_hapticEngine = [gamepad.haptics createEngineWithLocality:locality];
@@ -144,4 +149,22 @@
}
}
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftTrigger];
}
else {
return nil;
}
}
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightTrigger];
}
else {
return nil;
}
}
@end
+18
View File
@@ -0,0 +1,18 @@
//
// KeyboardInputField.h
// Moonlight
//
// Created by Cameron Gutman on 12/2/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import <Foundation/Foundation.h>
NS_ASSUME_NONNULL_BEGIN
@interface KeyboardInputField : UITextField
@end
NS_ASSUME_NONNULL_END
+18
View File
@@ -0,0 +1,18 @@
//
// KeyboardInputField.m
// Moonlight
//
// Created by Cameron Gutman on 12/2/22.
// Copyright © 2022 Moonlight Game Streaming Project. All rights reserved.
//
#import "KeyboardInputField.h"
@implementation KeyboardInputField
- (UIEditingInteractionConfiguration) editingInteractionConfiguration {
// Suppress the Undo menu that appears with a 3 finger tap
return UIEditingInteractionConfigurationNone;
}
@end
+1
View File
@@ -16,6 +16,7 @@ struct KeyEvent {
u_char modifier;
};
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4));
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
+39 -2
View File
@@ -11,7 +11,40 @@
@implementation KeyboardSupport
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)){
+ (BOOL)sendKeyEventForPress:(UIPress*)press down:(BOOL)down API_AVAILABLE(ios(13.4)) {
if (press.key != nil) {
return [KeyboardSupport sendKeyEvent:press.key down:down];
}
else {
short keyCode;
switch (press.type) {
case UIPressTypeUpArrow:
keyCode = 0x26;
break;
case UIPressTypeDownArrow:
keyCode = 0x28;
break;
case UIPressTypeLeftArrow:
keyCode = 0x25;
break;
case UIPressTypeRightArrow:
keyCode = 0x27;
break;
default:
// Unhandled press type
return NO;
}
LiSendKeyboardEvent(0x8000 | keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
0);
return YES;
}
}
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)) {
char modifierFlags = 0;
short keyCode = 0;
@@ -168,7 +201,8 @@
keyCode = 0x6B;
break;
case UIKeyboardHIDUsageKeypadEnter:
keyCode = 0x0D; // FIXME: Is this correct?
keyCode = 0x0D;
modifierFlags |= MODIFIER_EXTENDED;
break;
case UIKeyboardHIDUsageKeypadPeriod:
keyCode = 0x6E;
@@ -215,6 +249,9 @@
case UIKeyboardHIDUsageKeyboardRightAlt:
keyCode = 0xA5;
break;
case 669: // This value corresponds to the "Globe" or "Language" key on most Apple branded iPad keyboards.
keyCode = 0x1B; // This value corresponds to "Escape", which is missing from most Apple branded iPad keyboards.
break;
default:
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
assert(0);
+2 -1
View File
@@ -9,6 +9,7 @@
#import <Foundation/Foundation.h>
@class ControllerSupport;
@class StreamConfiguration;
@interface OnScreenControls : NSObject
@@ -24,7 +25,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
};
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
+35 -27
View File
@@ -68,6 +68,7 @@
ControllerSupport *_controllerSupport;
Controller *_controller;
NSMutableArray* _deadTouches;
BOOL _swapABXY;
}
static const float EDGE_WIDTH = .05;
@@ -111,12 +112,13 @@ static float L2_Y;
static float L3_X;
static float L3_Y;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport {
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig {
self = [self init];
_view = view;
_controllerSupport = controllerSupport;
_controller = [controllerSupport getOscController];
_deadTouches = [[NSMutableArray alloc] init];
_swapABXY = streamConfig.swapABXYButtons;
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
@@ -372,27 +374,33 @@ static float L3_Y;
}
- (void) drawButtons {
// create A button
UIImage* aButtonImage = [UIImage imageNamed:@"AButton"];
UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
CGRect aButtonFrame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
CGRect bButtonFrame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
CGRect xButtonFrame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
CGRect yButtonFrame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
// create A button
_aButton.contents = (id) aButtonImage.CGImage;
_aButton.frame = CGRectMake(BUTTON_CENTER_X - aButtonImage.size.width / 2, BUTTON_CENTER_Y + BUTTON_DIST, aButtonImage.size.width, aButtonImage.size.height);
_aButton.frame = _swapABXY ? bButtonFrame : aButtonFrame;
[_view.layer addSublayer:_aButton];
// create B button
UIImage* bButtonImage = [UIImage imageNamed:@"BButton"];
_bButton.frame = CGRectMake(BUTTON_CENTER_X + BUTTON_DIST, BUTTON_CENTER_Y - bButtonImage.size.height / 2, bButtonImage.size.width, bButtonImage.size.height);
_bButton.frame = _swapABXY ? aButtonFrame : bButtonFrame;
_bButton.contents = (id) bButtonImage.CGImage;
[_view.layer addSublayer:_bButton];
// create X Button
UIImage* xButtonImage = [UIImage imageNamed:@"XButton"];
_xButton.frame = CGRectMake(BUTTON_CENTER_X - BUTTON_DIST - xButtonImage.size.width, BUTTON_CENTER_Y - xButtonImage.size.height/ 2, xButtonImage.size.width, xButtonImage.size.height);
_xButton.frame = _swapABXY ? yButtonFrame : xButtonFrame;
_xButton.contents = (id) xButtonImage.CGImage;
[_view.layer addSublayer:_xButton];
// create Y Button
UIImage* yButtonImage = [UIImage imageNamed:@"YButton"];
_yButton.frame = CGRectMake(BUTTON_CENTER_X - yButtonImage.size.width / 2, BUTTON_CENTER_Y - BUTTON_DIST - yButtonImage.size.height, yButtonImage.size.width, yButtonImage.size.height);
_yButton.frame = _swapABXY ? xButtonFrame : yButtonFrame;
_yButton.contents = (id) yButtonImage.CGImage;
[_view.layer addSublayer:_yButton];
@@ -655,63 +663,63 @@ static float L3_Y;
for (UITouch* touch in touches) {
CGPoint touchLocation = [touch locationInView:_view];
if ([_aButton.presentationLayer hitTest:touchLocation]) {
if (_aButton.superlayer != nil && [_aButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:A_FLAG];
_aTouch = touch;
updated = true;
} else if ([_bButton.presentationLayer hitTest:touchLocation]) {
} else if (_bButton.superlayer != nil && [_bButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:B_FLAG];
_bTouch = touch;
updated = true;
} else if ([_xButton.presentationLayer hitTest:touchLocation]) {
} else if (_xButton.superlayer != nil && [_xButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:X_FLAG];
_xTouch = touch;
updated = true;
} else if ([_yButton.presentationLayer hitTest:touchLocation]) {
} else if (_yButton.superlayer != nil && [_yButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:Y_FLAG];
_yTouch = touch;
updated = true;
} else if ([_upButton.presentationLayer hitTest:touchLocation]) {
} else if (_upButton.superlayer != nil && [_upButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:UP_FLAG];
_dpadTouch = touch;
updated = true;
} else if ([_downButton.presentationLayer hitTest:touchLocation]) {
} else if (_downButton.superlayer != nil && [_downButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:DOWN_FLAG];
_dpadTouch = touch;
updated = true;
} else if ([_leftButton.presentationLayer hitTest:touchLocation]) {
} else if (_leftButton.superlayer != nil && [_leftButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LEFT_FLAG];
_dpadTouch = touch;
updated = true;
} else if ([_rightButton.presentationLayer hitTest:touchLocation]) {
} else if (_rightButton.superlayer != nil && [_rightButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RIGHT_FLAG];
_dpadTouch = touch;
updated = true;
} else if ([_startButton.presentationLayer hitTest:touchLocation]) {
} else if (_startButton.superlayer != nil && [_startButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:PLAY_FLAG];
_startTouch = touch;
updated = true;
} else if ([_selectButton.presentationLayer hitTest:touchLocation]) {
} else if (_selectButton.superlayer != nil && [_selectButton.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:BACK_FLAG];
_selectTouch = touch;
updated = true;
} else if ([_l1Button.presentationLayer hitTest:touchLocation]) {
} else if (_l1Button.superlayer != nil && [_l1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:LB_FLAG];
_l1Touch = touch;
updated = true;
} else if ([_r1Button.presentationLayer hitTest:touchLocation]) {
} else if (_r1Button.superlayer != nil && [_r1Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport setButtonFlag:_controller flags:RB_FLAG];
_r1Touch = touch;
updated = true;
} else if ([_l2Button.presentationLayer hitTest:touchLocation]) {
} else if (_l2Button.superlayer != nil && [_l2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateLeftTrigger:_controller left:0xFF];
_l2Touch = touch;
updated = true;
} else if ([_r2Button.presentationLayer hitTest:touchLocation]) {
} else if (_r2Button.superlayer != nil && [_r2Button.presentationLayer hitTest:touchLocation]) {
[_controllerSupport updateRightTrigger:_controller right:0xFF];
_r2Touch = touch;
updated = true;
} else if ([_l3Button.presentationLayer hitTest:touchLocation]) {
} else if (_l3Button.superlayer != nil && [_l3Button.presentationLayer hitTest:touchLocation]) {
if (l3Set) {
[_controllerSupport clearButtonFlag:_controller flags:LS_CLK_FLAG];
_l3Button.borderWidth = 0.0f;
@@ -722,7 +730,7 @@ static float L3_Y;
l3Set = !l3Set;
_l3Touch = touch;
updated = true;
} else if ([_r3Button.presentationLayer hitTest:touchLocation]) {
} else if (_r3Button.superlayer != nil && [_r3Button.presentationLayer hitTest:touchLocation]) {
if (r3Set) {
[_controllerSupport clearButtonFlag:_controller flags:RS_CLK_FLAG];
_r3Button.borderWidth = 0.0f;
@@ -733,7 +741,7 @@ static float L3_Y;
r3Set = !r3Set;
_r3Touch = touch;
updated = true;
} else if ([_leftStick.presentationLayer hitTest:touchLocation]) {
} else if (_leftStick.superlayer != nil && [_leftStick.presentationLayer hitTest:touchLocation]) {
if (l3TouchStart != nil) {
// Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now
@@ -745,7 +753,7 @@ static float L3_Y;
}
_lsTouch = touch;
stickTouch = true;
} else if ([_rightStick.presentationLayer hitTest:touchLocation]) {
} else if (_rightStick.superlayer != nil && [_rightStick.presentationLayer hitTest:touchLocation]) {
if (r3TouchStart != nil) {
// Find elapsed time and convert to milliseconds
// Use (-) modifier to conversion since receiver is earlier than now
+5 -2
View File
@@ -18,6 +18,7 @@ static const int REFERENCE_HEIGHT = 720;
BOOL touchMoved;
BOOL isDragging;
NSTimer* dragTimer;
NSUInteger peakTouchCount;
#if TARGET_OS_TV
UIGestureRecognizer* remotePressRecognizer;
@@ -59,6 +60,7 @@ static const int REFERENCE_HEIGHT = 720;
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
touchMoved = false;
peakTouchCount = [[event allTouches] count];
if ([[event allTouches] count] == 1) {
UITouch *touch = [[event allTouches] anyObject];
originalLocation = touchLocation = [touch locationInView:view];
@@ -126,7 +128,7 @@ static const int REFERENCE_HEIGHT = 720;
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
} else if (!touchMoved) {
if ([[event allTouches] count] == 2) {
if (peakTouchCount == 2) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
Log(LOG_D, @"Sending right mouse button press");
@@ -137,7 +139,7 @@ static const int REFERENCE_HEIGHT = 720;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
} else if ([[event allTouches] count] == 1) {
} else if (peakTouchCount == 1) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
if (!self->isDragging){
Log(LOG_D, @"Sending left mouse button press");
@@ -175,6 +177,7 @@ static const int REFERENCE_HEIGHT = 720;
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
peakTouchCount = 0;
}
#if TARGET_OS_TV
+325 -58
View File
@@ -13,14 +13,16 @@
#import "KeyboardSupport.h"
#import "RelativeTouchHandler.h"
#import "AbsoluteTouchHandler.h"
#import "KeyboardInputField.h"
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
@implementation StreamView {
OnScreenControls* onScreenControls;
UITextField* keyInputField;
KeyboardInputField* keyInputField;
BOOL isInputingText;
NSMutableSet* keysDown;
float streamAspectRatio;
@@ -52,7 +54,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
keyInputField = [[UITextField alloc] initWithFrame:CGRectZero];
keysDown = [[NSMutableSet alloc] init];
keyInputField = [[KeyboardInputField alloc] initWithFrame:CGRectZero];
[keyInputField setKeyboardType:UIKeyboardTypeDefault];
[keyInputField setAutocorrectionType:UITextAutocorrectionTypeNo];
[keyInputField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
@@ -71,7 +74,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
}
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport];
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport streamConfig:streamConfig];
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
if (settings.absoluteTouchMode) {
Log(LOG_I, @"On-screen controls disabled in absolute touch mode");
@@ -104,6 +107,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
[self addGestureRecognizer:continuousMouseWheelRecognizer];
}
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
if (@available(iOS 16.1, *)) {
UIHoverGestureRecognizer *stylusHoverRecognizer = [[UIHoverGestureRecognizer alloc] initWithTarget:self action:@selector(sendStylusHoverEvent:)];
stylusHoverRecognizer.allowedTouchTypes = @[@(UITouchTypePencil)];
[self addGestureRecognizer:stylusHoverRecognizer];
}
#endif
#endif
x1mouse = [[X1Mouse alloc] init];
@@ -165,6 +176,148 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
}
- (CGSize) getVideoAreaSize {
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
return CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
return CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
}
- (CGPoint) adjustCoordinatesForVideoArea:(CGPoint)point {
// These are now relative to the StreamView, however we need to scale them
// further to make them relative to the actual video portion.
float x = point.x - self.bounds.origin.x;
float y = point.y - self.bounds.origin.y;
// For some reason, we don't seem to always get to the bounds of the window
// so we'll subtract 1 pixel if we're to the left/below of the origin and
// and add 1 pixel if we're to the right/above. It should be imperceptible
// to the user but it will allow activation of gestures that require contact
// with the edge of the screen (like Aero Snap).
if (x < self.bounds.size.width / 2) {
x--;
}
else {
x++;
}
if (y < self.bounds.size.height / 2) {
y--;
}
else {
y++;
}
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize = [self getVideoAreaSize];
CGPoint videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Confine the cursor to the video region. We don't just discard events outside
// the region because we won't always get one exactly when the mouse leaves the region.
return CGPointMake(MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x,
MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y);
}
#if !TARGET_OS_TV
- (uint16_t)getRotationFromAzimuthAngle:(float)azimuthAngle {
// iOS reports azimuth of 0 when the stylus is pointing west, but Moonlight expects
// rotation of 0 to mean the stylus is pointing north. Rotate the azimuth angle
// clockwise by 90 degrees to convert from iOS to Moonlight rotation conventions.
int32_t rotationAngle = (azimuthAngle - M_PI_2) * (180.f / M_PI);
if (rotationAngle < 0) {
rotationAngle += 360;
}
return (uint16_t)rotationAngle;
}
- (uint8_t)getTiltFromAltitudeAngle:(float)altitudeAngle {
// iOS reports an altitude of 0 when the stylus is parallel to the touch surface,
// while Moonlight expects a tilt of 0 when the stylus is perpendicular to the surface.
// Subtract the tilt angle from 90 to convert from iOS to Moonlight tilt conventions.
uint8_t altitudeDegs = abs((int16_t)(altitudeAngle * (180.f / M_PI)));
return 90 - MIN(90, altitudeDegs);
}
- (BOOL)sendStylusEvent:(UITouch*)event {
uint8_t type;
// Don't touch stylus events if the host doesn't support them. We want to pass
// them as normal touches for legacy hosts that don't understand pen events.
if (!(LiGetHostFeatureFlags() & LI_FF_PEN_TOUCH_EVENTS)) {
return NO;
}
switch (event.phase) {
case UITouchPhaseBegan:
type = LI_TOUCH_EVENT_DOWN;
break;
case UITouchPhaseMoved:
type = LI_TOUCH_EVENT_MOVE;
break;
case UITouchPhaseEnded:
type = LI_TOUCH_EVENT_UP;
break;
case UITouchPhaseCancelled:
type = LI_TOUCH_EVENT_CANCEL;
break;
default:
return YES;
}
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
0.0f, 0.0f,
[self getRotationFromAzimuthAngle:[event azimuthAngleInView:self]],
[self getTiltFromAltitudeAngle:event.altitudeAngle]) != LI_ERR_UNSUPPORTED;
}
- (void)sendStylusHoverEvent:(UIHoverGestureRecognizer*)gesture API_AVAILABLE(ios(13.0)) {
uint8_t type;
switch (gesture.state) {
case UIGestureRecognizerStateBegan:
case UIGestureRecognizerStateChanged:
type = LI_TOUCH_EVENT_HOVER;
break;
case UIGestureRecognizerStateEnded:
type = LI_TOUCH_EVENT_HOVER_LEAVE;
break;
default:
return;
}
CGPoint location = [self adjustCoordinatesForVideoArea:[gesture locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
float distance = 0.0f;
#if defined(__IPHONE_16_1) || defined(__TVOS_16_1)
if (@available(iOS 16.1, *)) {
distance = gesture.zOffset;
}
#endif
uint16_t rotationAngle = LI_ROT_UNKNOWN;
uint8_t tiltAngle = LI_TILT_UNKNOWN;
#if defined(__IPHONE_16_4) || defined(__TVOS_16_4)
if (@available(iOS 16.4, *)) {
rotationAngle = [self getRotationFromAzimuthAngle:[gesture azimuthAngleInView:self]];
tiltAngle = [self getTiltFromAltitudeAngle:gesture.altitudeAngle];
}
#endif
LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
distance, 0.0f, 0.0f, rotationAngle, tiltAngle);
}
#endif
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
forTouches:touches
@@ -178,7 +331,26 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// Notify of user interaction and start expiration timer
[self startInteractionTimer];
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
}
}
#endif
if (![onScreenControls handleTouchDownEvent:touches]) {
// We still inform the touch handler even if we're going trigger the
// keyboard activation gesture. This is important to ensure the touch
// handler has a consistent view of touch events to correctly suppress
// activation of one or two finger gestures when a three finger gesture
// is triggered.
[touchHandler touchesBegan:touches withEvent:event];
if ([[event allTouches] count] == 3) {
if (isInputingText) {
Log(LOG_D, @"Closing the keyboard");
@@ -189,6 +361,23 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// Prepare the textbox used to capture keyboard events.
keyInputField.delegate = self;
keyInputField.text = @"0";
#if !TARGET_OS_TV
// Prepare the toolbar above the keyboard for more options
UIToolbar *customToolbarView = [[UIToolbar alloc] initWithFrame:CGRectMake(0, 0, self.bounds.size.width, 44)];
UIBarButtonItem *doneBarButton = [self createButtonWithImageNamed:@"DoneIcon.png" backgroundColor:[UIColor clearColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x00 isToggleable:NO];
UIBarButtonItem *windowsBarButton = [self createButtonWithImageNamed:@"WindowsIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x5B isToggleable:YES];
UIBarButtonItem *tabBarButton = [self createButtonWithImageNamed:@"TabIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x09 isToggleable:NO];
UIBarButtonItem *shiftBarButton = [self createButtonWithImageNamed:@"ShiftIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA0 isToggleable:YES];
UIBarButtonItem *escapeBarButton = [self createButtonWithImageNamed:@"EscapeIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x1B isToggleable:NO];
UIBarButtonItem *controlBarButton = [self createButtonWithImageNamed:@"ControlIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA2 isToggleable:YES];
UIBarButtonItem *altBarButton = [self createButtonWithImageNamed:@"AltIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0xA4 isToggleable:YES];
UIBarButtonItem *deleteBarButton = [self createButtonWithImageNamed:@"DeleteIcon.png" backgroundColor:[UIColor blackColor] target:self action:@selector(toolbarButtonClicked:) keyCode:0x2E isToggleable:NO];
UIBarButtonItem *flexibleSpace = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFlexibleSpace target:nil action:nil];
[customToolbarView setItems:[NSArray arrayWithObjects:doneBarButton, windowsBarButton, escapeBarButton, tabBarButton, shiftBarButton, controlBarButton, altBarButton, deleteBarButton, flexibleSpace, nil]];
keyInputField.inputAccessoryView = customToolbarView;
#endif
[keyInputField becomeFirstResponder];
[keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
@@ -198,8 +387,62 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
isInputingText = true;
}
}
}
}
- (UIBarButtonItem *)createButtonWithImageNamed:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor target:(id)target action:(SEL)action keyCode:(NSInteger)keyCode isToggleable:(BOOL)isToggleable {
UIImage *image = [UIImage imageNamed:imageName];
UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setImage:image forState:UIControlStateNormal];
button.frame = CGRectMake(0, 0, 30, 30);
button.imageView.contentMode = UIViewContentModeScaleAspectFit;
button.imageView.backgroundColor = backgroundColor;
button.imageView.layer.cornerRadius = 10.0;
button.imageEdgeInsets = UIEdgeInsetsMake(6, 6, 6, 6);
[button addTarget:target action:action forControlEvents:UIControlEventTouchUpInside];
objc_setAssociatedObject(button, "keyCode", @(keyCode), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(button, "isToggleable", @(isToggleable), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
objc_setAssociatedObject(button, "isOn", @(NO), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
UIBarButtonItem *barButton = [[UIBarButtonItem alloc] initWithCustomView:button];
return barButton;
}
- (void)toolbarButtonClicked:(UIButton *)sender {
BOOL isToggleable = [objc_getAssociatedObject(sender, "isToggleable") boolValue];
BOOL isOn = [objc_getAssociatedObject(sender, "isOn") boolValue];
if (isToggleable){
isOn = !isOn;
// Update the button's appearance based on its new state
if (isOn) {
sender.imageView.backgroundColor = [UIColor lightGrayColor];
} else {
sender.imageView.backgroundColor = [UIColor blackColor];
}
}
// Update the new on/off state of the button
objc_setAssociatedObject(sender, "isOn", @(isOn), OBJC_ASSOCIATION_RETAIN_NONATOMIC);
// Get the keyCode parameter and convert to short for key press event
short keyCode = [objc_getAssociatedObject(sender, "keyCode") shortValue];
// Close keyboard if done button clicked
if (!keyCode) {
[keyInputField resignFirstResponder];
isInputingText = false;
}
else {
// Send key press event using keyCode parameter, toggle if necessary
if (isToggleable){
if (isOn){
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
[keysDown addObject:@(keyCode)];
} else {
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
[keysDown removeObject:@(keyCode)];
}
}
else {
[touchHandler touchesBegan:touches withEvent:event];
LiSendKeyboardEvent(keyCode, KEY_ACTION_DOWN, 0);
usleep(50 * 1000);
LiSendKeyboardEvent(keyCode, KEY_ACTION_UP, 0);
}
}
}
@@ -268,6 +511,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
}
UITouch *touch = [touches anyObject];
if (touch.type == UITouchTypeIndirectPointer) {
if (@available(iOS 14.0, *)) {
@@ -303,7 +554,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
for (UIPress* press in presses) {
// For now, we'll treated it as handled if we handle at least one of the
// UIPress events inside the set.
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:YES]) {
if ([KeyboardSupport sendKeyEventForPress:press down:YES]) {
// This will prevent the legacy UITextField from receiving the event
handled = YES;
}
@@ -322,7 +573,7 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
for (UIPress* press in presses) {
// For now, we'll treated it as handled if we handle at least one of the
// UIPress events inside the set.
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:NO]) {
if ([KeyboardSupport sendKeyEventForPress:press down:NO]) {
// This will prevent the legacy UITextField from receiving the event
handled = YES;
}
@@ -346,6 +597,18 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
hasUserInteracted = YES;
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if ([self sendStylusEvent:touch]) {
return;
}
}
}
}
#endif
if (![onScreenControls handleTouchUpEvent:touches]) {
[touchHandler touchesEnded:touches withEvent:event];
}
@@ -356,48 +619,21 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
forTouches:touches
withEvent:event];
#if !TARGET_OS_TV
if (@available(iOS 13.4, *)) {
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
[self sendStylusEvent:touch];
}
}
}
#endif
}
#if !TARGET_OS_TV
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse {
// These are now relative to the StreamView, however we need to scale them
// further to make them relative to the actual video portion.
float x = location.x - self.bounds.origin.x;
float y = location.y - self.bounds.origin.y;
// For some reason, we don't seem to always get to the bounds of the window
// so we'll subtract 1 pixel if we're to the left/below of the origin and
// and add 1 pixel if we're to the right/above. It should be imperceptible
// to the user but it will allow activation of gestures that require contact
// with the edge of the screen (like Aero Snap).
if (x < self.bounds.size.width / 2) {
x--;
}
else {
x++;
}
if (y < self.bounds.size.height / 2) {
y--;
}
else {
y++;
}
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
CGSize videoSize;
CGPoint videoOrigin;
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
} else {
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
}
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
self.bounds.size.height / 2 - videoSize.height / 2);
// Confine the cursor to the video region. We don't just discard events outside
// the region because we won't always get one exactly when the mouse leaves the region.
x = MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width);
y = MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height);
CGPoint normalizedLocation = [self adjustCoordinatesForVideoArea:location];
CGSize videoSize = [self getVideoAreaSize];
// Send the mouse position relative to the video region if it has changed
// if we're receiving coordinates from a real mouse.
@@ -406,15 +642,14 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// send it if the value has changed. We will receive one of these events
// any time the user presses a modifier key, which can result in errant
// mouse motion when using a Citrix X1 mouse.
if (x != lastMouseX || y != lastMouseY || !isMouse) {
if (normalizedLocation.x != lastMouseX || normalizedLocation.y != lastMouseY || !isMouse) {
if (lastMouseX != 0 || lastMouseY != 0 || !isMouse) {
LiSendMousePositionEvent(x - videoOrigin.x, y - videoOrigin.y,
videoSize.width, videoSize.height);
LiSendMousePositionEvent(normalizedLocation.x, normalizedLocation.y, videoSize.width, videoSize.height);
}
if (isMouse) {
lastMouseX = x;
lastMouseY = y;
lastMouseX = normalizedLocation.x;
lastMouseY = normalizedLocation.y;
}
}
}
@@ -469,10 +704,23 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
CGPoint currentScrollTranslation = [gesture translationInView:self];
short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * 120; // WHEEL_DELTA
if (translationDeltaY != 0) {
LiSendHighResScrollEvent(translationDeltaY * 20);
lastScrollTranslation = currentScrollTranslation;
const short translationMultiplier = 120 * 20; // WHEEL_DELTA * 20
{
short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * translationMultiplier;
if (translationDeltaY != 0) {
LiSendHighResScrollEvent(translationDeltaY);
lastScrollTranslation = currentScrollTranslation;
}
}
{
short translationDeltaX = ((currentScrollTranslation.x - lastScrollTranslation.x) / self.bounds.size.width) * translationMultiplier;
if (translationDeltaX != 0) {
// Direction is reversed from vertical scrolling
LiSendHighResHScrollEvent(-translationDeltaX);
lastScrollTranslation = currentScrollTranslation;
}
}
}
@@ -492,9 +740,20 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
// Using velocityInView is 0 for discrete scroll events
// when scrolling very slowly, but translationInView does work.
CGPoint currentScrollTranslation = [gesture translationInView:self];
short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
if (translationDeltaY != 0) {
LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
{
short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
if (translationDeltaY != 0) {
LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
}
}
{
short translationDeltaX = currentScrollTranslation.x - lastScrollTranslation.x;
if (translationDeltaX != 0) {
// Direction is reversed from vertical scrolling
LiSendHScrollEvent(translationDeltaX < 0 ? 1 : -1);
}
}
lastScrollTranslation = currentScrollTranslation;
@@ -520,6 +779,13 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
return NO;
}
- (void)textFieldDidEndEditing:(UITextField *)textField {
for (NSNumber* keyCode in keysDown) {
LiSendKeyboardEvent([keyCode shortValue], KEY_ACTION_UP, 0);
}
[keysDown removeAllObjects];
}
- (void)onKeyboardPressed:(UITextField *)textField {
NSString* inputText = textField.text;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
@@ -579,9 +845,10 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if (event.modifier != 0) {
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
}
LiSendKeyboardEvent(event.keycode, KEY_ACTION_DOWN, event.modifier);
// Let the host know these are not (necessarily) normalized to US English scancodes
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_DOWN, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
usleep(50 * 1000);
LiSendKeyboardEvent(event.keycode, KEY_ACTION_UP, event.modifier);
LiSendKeyboardEvent2(event.keycode, KEY_ACTION_UP, event.modifier, SS_KBE_FLAG_NON_NORMALIZED);
if (event.modifier != 0) {
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
}
+2 -4
View File
@@ -32,13 +32,11 @@
<key>ProfileName</key>
<string>ExtendedGamepad</string>
</dict>
<dict>
<key>ProfileName</key>
<string>Gamepad</string>
</dict>
</array>
<key>GCSupportsControllerUserInteraction</key>
<true/>
<key>LSSupportsGameMode</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationCategoryType</key>
@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Moonlight v1.6.xcdatamodel</string>
<string>Moonlight v1.10.xcdatamodel</string>
</dict>
</plist>
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="22225" systemVersion="23A344" 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="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="deviceGyroMode" optional="YES" attributeType="Integer 32" defaultValueString="0" 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="preferredCodec" attributeType="Integer 32" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity>
</model>
@@ -0,0 +1,46 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="19574" systemVersion="20G224" 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="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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="118"/>
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="254"/>
</elements>
</model>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" 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="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" 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="118"/>
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
</elements>
</model>
@@ -0,0 +1,47 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="20086" systemVersion="21G72" 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="hidden" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="id" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
</entity>
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
<attribute name="name" attributeType="String" syncable="YES"/>
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
</entity>
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="audioConfig" attributeType="Integer 32" defaultValueString="2" usesScalarValueType="NO" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="statsOverlay" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="swapABXYButtons" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
<attribute name="useFramePacing" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="useHevc2" attributeType="Boolean" defaultValueString="YES" 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="118"/>
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="298"/>
</elements>
</model>
+1 -1
View File
@@ -20,7 +20,7 @@ static const int MAX_ATTEMPTS = 5;
- (void) main {
int attempts = 0;
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] serverCert:_host.serverCert];
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host];
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
+2 -2
View File
@@ -7,14 +7,14 @@
//
#import "AppListResponse.h"
#import "TemporaryHost.h"
#ifndef ConnectionHelper_h
#define ConnectionHelper_h
@interface ConnectionHelper : NSObject
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) serverCert uniqueID:(NSString*) uniqueId;
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host;
@end
+2 -2
View File
@@ -14,8 +14,8 @@
@implementation ConnectionHelper
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) cert uniqueID:(NSString*) uniqueId {
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
+(AppListResponse*) getAppListForHost:(TemporaryHost*)host {
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
// Try up to 5 times to get the app list
AppListResponse* appListResp = nil;
+2 -2
View File
@@ -120,7 +120,7 @@
}
- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address {
HttpManager* hMan = [[HttpManager alloc] initWithHost:address uniqueId:_uniqueId serverCert:nil];
HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:nil];
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
return serverInfoResponse;
@@ -202,7 +202,7 @@
callback(host, nil);
}
} else if (!prohibitedAddress) {
callback(nil, @"Could not connect to host. Ensure GameStream is enabled in GeForce Experience on your PC.");
callback(nil, @"Could not connect to host.\n\nIf you're hosting using GeForce Experience, make sure you've enabled the toggle on the SHIELD tab.\n\nIf you're hosting using Sunshine, ensure it is running properly. If you're using a non-default port, you will need to include that here.");
} else {
callback(nil, prohibitedAddressMessage);
}
+2 -4
View File
@@ -103,8 +103,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
receivedResponse = [self checkResponse:serverInfoResp];
if (receivedResponse) {
[serverInfoResp populateHost:_host];
_host.activeAddress = address;
[serverInfoResp populateHost:_host];
// Update the database using the response
DataManager *dataManager = [[DataManager alloc] init];
@@ -126,9 +126,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
}
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
uniqueId:_uniqueId
serverCert:cert];
HttpManager* hMan = [[HttpManager alloc] initWithAddress:address httpsPort:0 serverCert:cert];
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
withUrlRequest:[hMan newServerInfoRequest:true]
+4 -3
View File
@@ -9,10 +9,12 @@
#import "HttpResponse.h"
#import "HttpRequest.h"
#import "StreamConfiguration.h"
#import "TemporaryHost.h"
@interface HttpManager : NSObject <NSURLSessionDelegate>
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
- (id) initWithHost:(TemporaryHost*) host;
- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short) httpsPort serverCert:(NSData*) serverCert;
- (void) setServerCert:(NSData*) serverCert;
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
- (NSURLRequest*) newUnpairRequest;
@@ -24,8 +26,7 @@
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
- (NSURLRequest*) newHttpServerInfoRequest;
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config;
- (NSURLRequest*) newQuitAppRequest;
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
- (void) executeRequestSynchronously:(HttpRequest*)request;
+118 -56
View File
@@ -10,6 +10,7 @@
#import "HttpRequest.h"
#import "CryptoManager.h"
#import "TemporaryApp.h"
#import "ServerInfoResponse.h"
#include <libxml2/libxml/xmlreader.h>
#include <string.h>
@@ -22,22 +23,16 @@
#define EXTRA_LONG_TIMEOUT_SEC 180
@implementation HttpManager {
NSURLSession* _urlSession;
NSString* _urlSafeHostName;
NSString* _baseHTTPURL;
NSString* _baseHTTPSURL;
NSString* _uniqueId;
NSString* _deviceName;
NSData* _serverCert;
NSMutableData* _respData;
NSData* _requestResp;
dispatch_semaphore_t _requestLock;
NSError* _error;
TemporaryHost *_host; // May be nil
NSString* _baseHTTPSURL;
}
static const NSString* HTTP_PORT = @"47989";
static const NSString* HTTPS_PORT = @"47984";
+ (NSData*) fixXmlVersion:(NSData*) xmlData {
NSString* dataString = [[NSString alloc] initWithData:xmlData encoding:NSUTF8StringEncoding];
NSString* xmlString = [dataString stringByReplacingOccurrencesOfString:@"UTF-16" withString:@"UTF-8" options:NSCaseInsensitiveSearch range:NSMakeRange(0, [dataString length])];
@@ -49,63 +44,111 @@ static const NSString* HTTPS_PORT = @"47984";
_serverCert = serverCert;
}
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert {
- (id) initWithHost:(TemporaryHost*) host {
self = [self initWithAddress:host.activeAddress httpsPort:host.httpsPort serverCert:host.serverCert];
_host = host;
return self;
}
- (id) initWithAddress:(NSString*) hostAddressPortString httpsPort:(unsigned short)httpsPort serverCert:(NSData*) serverCert {
self = [super init];
// Use the same UID for all Moonlight clients to allow them
// quit games started on another Moonlight client.
_uniqueId = @"0123456789ABCDEF";
_deviceName = deviceName;
_serverCert = serverCert;
_requestLock = dispatch_semaphore_create(0);
_respData = [[NSMutableData alloc] init];
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
NSString* address = [Utils addressPortStringToAddress:hostAddressPortString];
unsigned short port = [Utils addressPortStringToPort:hostAddressPortString];
// If this is an IPv6 literal, we must properly enclose it in brackets
NSString* urlSafeHost;
if ([host containsString:@":"]) {
urlSafeHost = [NSString stringWithFormat:@"[%@]", host];
if ([address containsString:@":"]) {
_urlSafeHostName = [NSString stringWithFormat:@"[%@]", address];
} else {
urlSafeHost = host;
_urlSafeHostName = address;
}
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", urlSafeHost, HTTP_PORT];
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", urlSafeHost, HTTPS_PORT];
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%u", _urlSafeHostName, port];
if (httpsPort) {
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, httpsPort];
}
return self;
}
- (BOOL) ensureHttpsUrlPopulated:(bool)fastFail {
if (!_baseHTTPSURL) {
// Use the caller's provided port if one was specified
if (_host && _host.httpsPort != 0) {
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, _host.httpsPort];
}
else {
// Query the host to retrieve the HTTPS port
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
[self executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[self newHttpServerInfoRequest:false]]];
TemporaryHost* dummyHost = [[TemporaryHost alloc] init];
if (![serverInfoResponse isStatusOk]) {
return NO;
}
[serverInfoResponse populateHost:dummyHost];
// Pass the port back if the caller provided storage for it
if (_host) {
_host.httpsPort = dummyHost.httpsPort;
}
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%u", _urlSafeHostName, dummyHost.httpsPort];
}
}
return YES;
}
- (void) executeRequestSynchronously:(HttpRequest*)request {
[_respData setLength:0];
_error = nil;
// This is a special case to handle failure of HTTPS port fetching
if (!request.request) {
if (request.response) {
request.response.statusCode = EHOSTDOWN;
request.response.statusMessage = @"Host is unreachable";
}
return;
}
__block NSData* requestResp = nil;
__block NSError* respError = nil;
__block dispatch_semaphore_t requestLock = dispatch_semaphore_create(0);
Log(LOG_D, @"Making Request: %@", request);
[[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
NSURLSession* urlSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration] delegate:self delegateQueue:nil];
[[urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
if (error != NULL) {
Log(LOG_D, @"Connection error: %@", error);
self->_error = error;
respError = 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]);
[self->_respData appendData:data];
if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
if ([[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding] != nil) {
requestResp = [HttpManager fixXmlVersion:data];
} else {
self->_requestResp = self->_respData;
requestResp = data;
}
}
}
dispatch_semaphore_signal(self->_requestLock);
dispatch_semaphore_signal(requestLock);
}] resume];
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
if (!_error && request.response) {
[request.response populateWithData:_requestResp];
dispatch_semaphore_wait(requestLock, DISPATCH_TIME_FOREVER);
[urlSession invalidateAndCancel];
if (!respError && request.response) {
[request.response populateWithData:requestResp];
// If the fallback error code was detected, issue the fallback request
if (request.response.statusCode == request.fallbackError && request.fallbackRequest != NULL) {
@@ -116,7 +159,7 @@ static const NSString* HTTPS_PORT = @"47984";
[self executeRequestSynchronously:request];
}
}
else if (_error && [_error code] == NSURLErrorServerCertificateUntrusted) {
else if (respError && [respError 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);
@@ -131,9 +174,9 @@ static const NSString* HTTPS_PORT = @"47984";
[self executeRequestSynchronously:request];
}
}
else if (_error && request.response) {
request.response.statusCode = [_error code];
request.response.statusMessage = [_error localizedDescription];
else if (respError && request.response) {
request.response.statusCode = [respError code];
request.response.statusMessage = [respError localizedDescription];
}
}
@@ -174,11 +217,19 @@ static const NSString* HTTPS_PORT = @"47984";
}
- (NSURLRequest*) newPairChallenge {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
- (NSURLRequest *)newAppListRequest {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
@@ -189,6 +240,10 @@ static const NSString* HTTPS_PORT = @"47984";
return [self newHttpServerInfoRequest:fastFail];
}
if (![self ensureHttpsUrlPopulated:fastFail]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
}
@@ -202,44 +257,49 @@ static const NSString* HTTPS_PORT = @"47984";
return [self newHttpServerInfoRequest:false];
}
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
- (NSURLRequest*) newLaunchOrResumeRequest:(NSString*)verb config:(StreamConfiguration*)config {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
// Using an FPS value over 60 causes SOPS to default to 720p60,
// so force it to 0 to ensure the correct resolution is set. We
// used to use 60 here but that locked the frame rate to 60 FPS
// on GFE 3.20.3.
int fps = config.frameRate > 60 ? 0 : config.frameRate;
// on GFE 3.20.3. We do not do this hack for Sunshine (which is
// indicated by a negative version in the last field.
int fps = (config.frameRate > 60 && ![config.appVersion containsString:@".-"]) ? 0 : config.frameRate;
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d",
_baseHTTPSURL, _uniqueId,
NSString* urlString = [NSString stringWithFormat:@"%@/%@?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d&gcpersist=%d%s",
_baseHTTPSURL, verb, _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.supportedVideoFormats & VIDEO_FORMAT_MASK_10BIT) ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
config.playAudioOnPC ? 1 : 0,
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
config.gamepadMask, config.gamepadMask];
config.gamepadMask, config.gamepadMask,
!config.multiController ? 1 : 0,
LiGetLaunchUrlQueryParameters()];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is launching
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
_baseHTTPSURL, _uniqueId,
[Utils bytesToHex:config.riKey], config.riKeyId,
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration)];
Log(LOG_I, @"Requesting: %@", urlString);
// This blocks while the app is resuming
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newQuitAppRequest {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
}
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
if (![self ensureHttpsUrlPopulated:NO]) {
return nil;
}
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
}
@@ -259,7 +319,7 @@ static const NSString* HTTPS_PORT = @"47984";
SecIdentityCopyCertificate(identity, &certificate);
return [[NSArray alloc] initWithObjects:(__bridge id)certificate, nil];
return [[NSArray alloc] initWithObjects:(__bridge_transfer id)certificate, nil];
}
// Returns the identity
@@ -271,13 +331,14 @@ static const NSString* HTTPS_PORT = @"47984";
const void *keys[] = { kSecImportExportPassphrase };
const void *values[] = { password };
CFDictionaryRef options = CFDictionaryCreate(NULL, keys, values, 1, NULL, NULL);
CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
CFArrayRef items = nil;
OSStatus securityError = SecPKCS12Import(p12Data, options, &items);
if (securityError == errSecSuccess) {
//Log(LOG_D, @"Success opening p12 certificate. Items: %ld", CFArrayGetCount(items));
CFDictionaryRef identityDict = CFArrayGetValueAtIndex(items, 0);
identityApp = (SecIdentityRef)CFDictionaryGetValue(identityDict, kSecImportItemIdentity);
identityApp = (SecIdentityRef)CFRetain(CFDictionaryGetValue(identityDict, kSecImportItemIdentity));
CFRelease(items);
} else {
Log(LOG_E, @"Error opening Certificate.");
}
@@ -331,6 +392,7 @@ static const NSString* HTTPS_PORT = @"47984";
SecIdentityRef identity = [self getClientCertificate];
NSArray* certArray = [self getCertificate:identity];
NSURLCredential* newCredential = [NSURLCredential credentialWithIdentity:identity certificates:certArray persistence:NSURLCredentialPersistencePermanent];
CFRelease(identity);
completionHandler(NSURLSessionAuthChallengeUseCredential, newCredential);
}
else
+14 -9
View File
@@ -72,16 +72,21 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
if (addr->sa_family == AF_INET) {
inet_ntop(addr->sa_family, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
unsigned short port = ntohs(((struct sockaddr_in*)addr)->sin_port);
return [NSString stringWithFormat: @"%s:%u", addrStr, port];
}
else {
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
inet_ntop(addr->sa_family, &sin6->sin6_addr, addrStr, sizeof(addrStr));
unsigned short port = ntohs(((struct sockaddr_in6*)addr)->sin6_port);
if (sin6->sin6_scope_id != 0) {
// Link-local addresses with scope IDs are special
return [NSString stringWithFormat: @"%s%%%u", addrStr, sin6->sin6_scope_id];
return [NSString stringWithFormat: @"[%s%%%u]:%u", addrStr, sin6->sin6_scope_id, port];
}
else {
return [NSString stringWithFormat: @"[%s]:%u", addrStr, port];
}
}
return [NSString stringWithFormat: @"%s", addrStr];
}
+ (BOOL)isAddress:(uint8_t*)address inSubnet:(uint8_t*)subnet netmask:(int)bits {
@@ -235,7 +240,7 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
[NSTimer scheduledTimerWithTimeInterval:2.0
target:self
selector:@selector(retryResolveTimerCallback:)
userInfo:nil
userInfo:sender
repeats:NO];
}
@@ -289,12 +294,12 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
return;
}
Log(LOG_I, @"Retrying mDNS resolution");
for (NSNetService* service in services) {
if (service.hostName == nil) {
[service setDelegate:self];
[service resolveWithTimeout:5];
}
NSNetService* service = timer.userInfo;
Log(LOG_I, @"Retrying mDNS resolution for %@", service);
if (service.hostName == nil) {
[service setDelegate:self];
[service resolveWithTimeout:5];
}
}
+12 -8
View File
@@ -38,15 +38,13 @@
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
if ([serverInfoResp isStatusOk]) {
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
[_callback pairFailed:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
} else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
if (appversion == nil) {
[_callback pairFailed:@"Missing XML element"];
return;
}
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue]];
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue] withState:[serverInfoResp getStringTag:@"state"]];
} else {
[_callback alreadyPaired];
}
@@ -82,8 +80,8 @@
}
// All codepaths must call finishPairing exactly once before returning!
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion {
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion withState:(NSString*)state {
Log(LOG_I, @"Pairing with generation %d server in state %@", serverMajorVersion, state);
// Start a background task to help prevent the app from being killed
// while pairing is in progress.
@@ -99,7 +97,13 @@
HttpResponse* pairResp = [[HttpResponse alloc] init];
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
if (![self verifyResponseStatus:pairResp]) {
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
// GFE does not allow pairing while a server is busy, but Sunshine does. We give it a try and display the busy error if it fails.
if ([state hasSuffix:@"_SERVER_BUSY"]) {
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
}
else {
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
}
return;
}
@@ -168,7 +172,7 @@
NSData* serverSecretResp = [Utils hexToBytes:[secretResp getStringTag:@"pairingsecret"]];
NSData* serverSecret = [serverSecretResp subdataWithRange:NSMakeRange(0, 16)];
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, serverSecretResp.length - 16)];
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
+4
View File
@@ -11,6 +11,7 @@
#define TAG_HOSTNAME @"hostname"
#define TAG_EXTERNAL_IP @"ExternalIP"
#define TAG_HTTPS_PORT @"HttpsPort"
#define TAG_LOCAL_IP @"LocalIP"
#define TAG_UNIQUE_ID @"uniqueid"
#define TAG_MAC_ADDRESS @"mac"
@@ -18,6 +19,9 @@
#define TAG_STATE @"state"
#define TAG_CURRENT_GAME @"currentgame"
// Sunshine extension
#define TAG_EXTERNAL_PORT @"ExternalPort"
@interface ServerInfoResponse : HttpResponse <Response>
- (void) populateWithData:(NSData *)data;
+46 -2
View File
@@ -23,17 +23,58 @@
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
NSInteger httpsPort;
if ([self getIntTag:TAG_HTTPS_PORT value:&httpsPort]) {
host.httpsPort = (unsigned short)httpsPort;
}
else {
// Use the default if it's not specified
host.httpsPort = 47984;
}
// We might get an IPv4 loopback address if we're using GS IPv6 Forwarder
NSString *lanAddr = [[self getStringTag:TAG_LOCAL_IP] trim];
if (![lanAddr hasPrefix:@"127."]) {
host.localAddress = lanAddr;
unsigned short localPort;
// If we reached this host through this port, store our port there
if (host.activeAddress && [lanAddr isEqualToString:[Utils addressPortStringToAddress:host.activeAddress]]) {
localPort = [Utils addressPortStringToPort:host.activeAddress];
}
else if (host.localAddress) {
// If there's an existing local address, use the port from that
localPort = [Utils addressPortStringToPort:host.localAddress];
}
else {
// If all else fails, use 47989
localPort = 47989;
}
host.localAddress = [Utils addressAndPortToAddressPortString:lanAddr port:localPort];
}
// This is a Sunshine extension for WAN port remapping
NSInteger externalHttpPort;
if (![self getIntTag:TAG_EXTERNAL_PORT value:&externalHttpPort]) {
// Use our active port if it's not specified
if (host.activeAddress) {
externalHttpPort = [Utils addressPortStringToPort:host.activeAddress];
}
else {
// Otherwise use the default
externalHttpPort = 47989;
}
}
// 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;
host.externalAddress = [Utils addressAndPortToAddressPortString:wanAddr port:externalHttpPort];
}
else if (host.externalAddress) {
// If we have an external address (via STUN) already, we still need to populate the port
host.externalAddress = [Utils addressAndPortToAddressPortString:[Utils addressPortStringToAddress:host.externalAddress] port:externalHttpPort];
}
NSString *state = [[self getStringTag:TAG_STATE] trim];
@@ -44,6 +85,9 @@
host.currentGame = @"0";
}
// GFE uses the Mjolnir codename in their state enum values
host.isNvidiaServerSoftware = [state containsString:@"MJOLNIR"];
NSInteger pairStatus;
if ([self getIntTag:TAG_PAIR_STATUS value:&pairStatus]) {
host.pairState = pairStatus ? PairStatePaired : PairStateUnpaired;
+34 -13
View File
@@ -16,12 +16,15 @@
@implementation WakeOnLanManager
static const int numPorts = 7;
static const int ports[numPorts] = {
static const int numStaticPorts = 2;
static const int staticPorts[numStaticPorts] = {
9, // Standard WOL port (privileged port)
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
};
static const int numDynamicPorts = 5;
static const int dynamicPorts[numDynamicPorts] = {
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE/Sunshine
};
+ (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
if (addr->ss_family == AF_INET) {
@@ -38,29 +41,33 @@ static const int ports[numPorts] = {
NSData* wolPayload = [WakeOnLanManager createPayload:host];
for (int i = 0; i < 5; i++) {
const char* address;
NSString* address;
struct addrinfo hints, *res, *curr;
// try all ip addresses
if (i == 0 && host.localAddress != nil) {
address = [host.localAddress UTF8String];
address = host.localAddress;
} else if (i == 1 && host.externalAddress != nil) {
address = [host.externalAddress UTF8String];
address = host.externalAddress;
} else if (i == 2 && host.address != nil) {
address = [host.address UTF8String];
address = host.address;
} else if (i == 3 && host.ipv6Address != nil) {
address = [host.ipv6Address UTF8String];
address = host.ipv6Address;
} else if (i == 4) {
address = "255.255.255.255";
address = @"255.255.255.255";
} else {
// Requested address wasn't present
continue;
}
// Get the raw address and base port from the address+port string
NSString* rawAddress = [Utils addressPortStringToAddress:address];
unsigned short basePort = [Utils addressPortStringToPort:address];
memset(&hints, 0, sizeof(hints));
hints.ai_family = AF_UNSPEC;
hints.ai_flags = AI_ADDRCONFIG;
if (getaddrinfo(address, NULL, &hints, &res) != 0 || res == NULL) {
if (getaddrinfo([rawAddress UTF8String], NULL, &hints, &res) != 0 || res == NULL) {
// Failed to resolve address
Log(LOG_E, @"Failed to resolve WOL address");
continue;
@@ -86,15 +93,29 @@ static const int ports[numPorts] = {
memset(&addr, 0, sizeof(addr));
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
for (int j = 0; j < numPorts; j++) {
[WakeOnLanManager populateAddress:&addr withPort:ports[j]];
for (int j = 0; j < numStaticPorts; j++) {
[WakeOnLanManager populateAddress:&addr withPort:staticPorts[j]];
long err = sendto(wolSocket,
[wolPayload bytes],
[wolPayload length],
0,
(struct sockaddr*)&addr,
curr->ai_addrlen);
Log(LOG_I, @"Sending WOL packet returned: %ld", err);
Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", staticPorts[j], err);
}
for (int j = 0; j < numDynamicPorts; j++) {
// Offset the WoL dynamic ports by the base port
unsigned short port = ((int)dynamicPorts[j] - 47989) + basePort;
[WakeOnLanManager populateAddress:&addr withPort:port];
long err = sendto(wolSocket,
[wolPayload bytes],
[wolPayload length],
0,
(struct sockaddr*)&addr,
curr->ai_addrlen);
Log(LOG_I, @"Sending WOL packet to port %u returned: %ld", port, err);
}
close(wolSocket);
+4
View File
@@ -17,6 +17,10 @@ typedef struct {
int totalFrames;
int receivedFrames;
int networkDroppedFrames;
int totalHostProcessingLatency;
int framesWithHostProcessingLatency;
int maxHostProcessingLatency;
int minHostProcessingLatency;
} video_stats_t;
@interface Connection : NSOperation <NSStreamDelegate>
+114 -66
View File
@@ -47,7 +47,7 @@ static VideoDecoderRenderer* renderer;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
{
[renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate];
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
lastFrameNumber = 0;
activeVideoFormat = videoFormat;
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
@@ -55,9 +55,14 @@ int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void*
return 0;
}
void DrCleanup(void)
void DrStart(void)
{
[renderer cleanup];
[renderer start];
}
void DrStop(void)
{
[renderer stop];
}
-(BOOL) getVideoStats:(video_stats_t*)stats
@@ -81,10 +86,44 @@ void DrCleanup(void)
{
case VIDEO_FORMAT_H264:
return @"H.264";
case VIDEO_FORMAT_H264_HIGH8_444:
return @"H.264 (4:4:4)";
case VIDEO_FORMAT_H265:
return @"HEVC";
case VIDEO_FORMAT_H265_REXT8_444:
return @"HEVC (4:4:4)";
case VIDEO_FORMAT_H265_MAIN10:
return @"HEVC Main 10";
if (LiGetCurrentHostDisplayHdrMode()) {
return @"HEVC Main 10 HDR";
}
else {
return @"HEVC Main 10 SDR";
}
case VIDEO_FORMAT_H265_REXT10_444:
if (LiGetCurrentHostDisplayHdrMode()) {
return @"HEVC Main 10 HDR (4:4:4)";
}
else {
return @"HEVC Main 10 SDR (4:4:4)";
}
case VIDEO_FORMAT_AV1_MAIN8:
return @"AV1";
case VIDEO_FORMAT_AV1_HIGH8_444:
return @"AV1 (4:4:4)";
case VIDEO_FORMAT_AV1_MAIN10:
if (LiGetCurrentHostDisplayHdrMode()) {
return @"AV1 10-bit HDR";
}
else {
return @"AV1 10-bit SDR";
}
case VIDEO_FORMAT_AV1_HIGH10_444:
if (LiGetCurrentHostDisplayHdrMode()) {
return @"AV1 10-bit HDR (4:4:4)";
}
else {
return @"AV1 10-bit SDR (4:4:4)";
}
default:
return @"UNKNOWN";
}
@@ -124,6 +163,19 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
lastFrameNumber = decodeUnit->frameNumber;
}
if (decodeUnit->frameHostProcessingLatency != 0) {
if (currentVideoStats.minHostProcessingLatency == 0 || decodeUnit->frameHostProcessingLatency < currentVideoStats.minHostProcessingLatency) {
currentVideoStats.minHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
}
if (decodeUnit->frameHostProcessingLatency > currentVideoStats.maxHostProcessingLatency) {
currentVideoStats.maxHostProcessingLatency = decodeUnit->frameHostProcessingLatency;
}
currentVideoStats.framesWithHostProcessingLatency++;
currentVideoStats.totalHostProcessingLatency += decodeUnit->frameHostProcessingLatency;
}
currentVideoStats.receivedFrames++;
currentVideoStats.totalFrames++;
@@ -134,8 +186,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length
bufferType:entry->bufferType
frameType:decodeUnit->frameType
pts:decodeUnit->presentationTimeMs];
decodeUnit:decodeUnit];
if (ret != DR_OK) {
free(data);
return ret;
@@ -153,8 +204,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return [renderer submitDecodeBuffer:data
length:offset
bufferType:BUFFER_TYPE_PICDATA
frameType:decodeUnit->frameType
pts:decodeUnit->presentationTimeMs];
decodeUnit:decodeUnit];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
@@ -171,23 +221,23 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
// This is supposed to be a power of 2, but our
// frames contain a non-power of 2 number of samples,
// so the slop would require buffering another full frame.
// Specifying non-Po2 seems to work for our supported platforms.
want.samples = opusConfig->samplesPerFrame;
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
if (audioDevice == 0) {
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
SDL_QuitSubSystem(SDL_INIT_AUDIO);
ArCleanup();
return -1;
}
audioConfig = *opusConfig;
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
audioBuffer = SDL_malloc(audioFrameSize);
if (audioBuffer == NULL) {
Log(LOG_E, @"Failed to allocate audio frame buffer");
ArCleanup();
return -1;
}
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
@@ -195,10 +245,18 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
opusConfig->coupledStreams,
opusConfig->mapping,
&err);
if (opusDecoder == NULL) {
Log(LOG_E, @"Failed to create Opus decoder");
ArCleanup();
return -1;
}
// Start playback
SDL_PauseAudioDevice(audioDevice, 0);
// Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default)
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
return 0;
}
@@ -292,6 +350,27 @@ void ClConnectionStatusUpdate(int status)
[_callbacks connectionStatusUpdate:status];
}
void ClSetHdrMode(bool enabled)
{
[renderer setHdrMode:enabled];
[_callbacks setHdrMode:enabled];
}
void ClRumbleTriggers(uint16_t controllerNumber, uint16_t leftTriggerMotor, uint16_t rightTriggerMotor)
{
[_callbacks rumbleTriggers:controllerNumber leftTrigger:leftTriggerMotor rightTrigger:rightTriggerMotor];
}
void ClSetMotionEventState(uint16_t controllerNumber, uint8_t motionType, uint16_t reportRateHz)
{
[_callbacks setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
}
void ClSetControllerLED(uint16_t controllerNumber, uint8_t r, uint8_t g, uint8_t b)
{
[_callbacks setControllerLed:controllerNumber r:r g:g b:b];
}
-(void) terminate
{
// Interrupt any action blocking LiStartConnection(). This is
@@ -324,21 +403,22 @@ void ClConnectionStatusUpdate(int status)
videoStatsLock = [[NSLock alloc] init];
}
NSString *rawAddress = [Utils addressPortStringToAddress:config.host];
strncpy(_hostString,
[config.host cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_hostString));
[rawAddress cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_hostString) - 1);
strncpy(_appVersionString,
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_appVersionString));
sizeof(_appVersionString) - 1);
if (config.gfeVersion != nil) {
strncpy(_gfeVersionString,
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_gfeVersionString));
sizeof(_gfeVersionString) - 1);
}
if (config.rtspSessionUrl != nil) {
strncpy(_rtspSessionUrl,
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
sizeof(_rtspSessionUrl));
sizeof(_rtspSessionUrl) - 1);
}
LiInitializeServerInformation(&_serverInfo);
@@ -350,6 +430,7 @@ void ClConnectionStatusUpdate(int status)
if (config.rtspSessionUrl != nil) {
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
}
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
renderer = myRenderer;
_callbacks = callbacks;
@@ -359,17 +440,13 @@ void ClConnectionStatusUpdate(int status)
_streamConfig.height = config.height;
_streamConfig.fps = config.frameRate;
_streamConfig.bitrate = config.bitRate;
_streamConfig.enableHdr = config.enableHdr;
_streamConfig.supportedVideoFormats = config.supportedVideoFormats;
_streamConfig.audioConfiguration = config.audioConfiguration;
// TODO: If/when video encryption is added, we'll probably want to
// limit that to devices that support the ARMv8 AES instructions.
_streamConfig.encryptionFlags = ENCFLG_AUDIO;
// Use some of the HEVC encoding efficiency improvements to
// reduce bandwidth usage while still gaining some image
// quality improvement.
_streamConfig.hevcBitratePercentageMultiplier = 75;
// Since we require iOS 12 or above, we're guaranteed to be running
// on a 64-bit device with ARMv8 crypto instructions, so we don't
// need to check for that here.
_streamConfig.encryptionFlags = ENCFLG_ALL;
if ([Utils isActiveNetworkVPN]) {
// Force remote streaming mode when a VPN is connected
@@ -381,32 +458,6 @@ void ClConnectionStatusUpdate(int status)
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
_streamConfig.packetSize = 1392;
}
// HDR implies HEVC allowed
if (config.enableHdr) {
config.allowHevc = YES;
}
// Streaming at resolutions above 4K requires HEVC
if (config.width > 4096 || config.height > 4096) {
config.allowHevc = YES;
}
// 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, tvOS 11.3, *)) {
_streamConfig.supportsHevc =
#if !TARGET_OS_TV
config.allowHevc &&
#endif
VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
}
// 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);
@@ -415,18 +466,11 @@ void ClConnectionStatusUpdate(int status)
LiInitializeVideoCallbacks(&_drCallbacks);
_drCallbacks.setup = DrDecoderSetup;
_drCallbacks.cleanup = DrCleanup;
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
// RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
// It doesnt work on macOS either, tested with Network Link Conditioner.
// RFI seems to be broken at all resolutions on the Apple TV 4K (1st gen)
// on tvOS 14.5.
_drCallbacks.capabilities =
#if !TARGET_OS_TV
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
#endif
CAPABILITY_DIRECT_SUBMIT;
_drCallbacks.start = DrStart;
_drCallbacks.stop = DrStop;
_drCallbacks.capabilities = CAPABILITY_PULL_RENDERER |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_HEVC |
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AV1;
LiInitializeAudioCallbacks(&_arCallbacks);
_arCallbacks.init = ArInit;
@@ -443,6 +487,10 @@ void ClConnectionStatusUpdate(int status)
_clCallbacks.logMessage = ClLogMessage;
_clCallbacks.rumble = ClRumble;
_clCallbacks.connectionStatusUpdate = ClConnectionStatusUpdate;
_clCallbacks.setHdrMode = ClSetHdrMode;
_clCallbacks.rumbleTriggers = ClRumbleTriggers;
_clCallbacks.setMotionEventState = ClSetMotionEventState;
_clCallbacks.setControllerLED = ClSetControllerLED;
return self;
}
+4
View File
@@ -16,6 +16,10 @@
- (void) launchFailed:(NSString*)message;
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
- (void) connectionStatusUpdate:(int)status;
- (void) setHdrMode:(bool)enabled;
- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger;
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz;
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b;
- (void) videoContentShown;
@end
+5 -2
View File
@@ -9,11 +9,13 @@
@interface StreamConfiguration : NSObject
@property NSString* host;
@property unsigned short httpsPort;
@property NSString* appVersion;
@property NSString* gfeVersion;
@property NSString* appID;
@property NSString* appName;
@property NSString* rtspSessionUrl;
@property int serverCodecModeSupport;
@property int width;
@property int height;
@property int frameRate;
@@ -23,10 +25,11 @@
@property int gamepadMask;
@property BOOL optimizeGameSettings;
@property BOOL playAudioOnPC;
@property BOOL swapABXYButtons;
@property int audioConfiguration;
@property BOOL enableHdr;
@property int supportedVideoFormats;
@property BOOL multiController;
@property BOOL allowHevc;
@property BOOL useFramePacing;
@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, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, audioConfiguration, enableHdr, multiController, allowHevc, serverCert, rtspSessionUrl;
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport;
@end
+24 -13
View File
@@ -39,10 +39,8 @@
- (void)main {
[CryptoManager generateKeyPairUsingSSL];
NSString* uniqueId = [IdManager getUniqueId];
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
uniqueId:uniqueId
HttpManager* hMan = [[HttpManager alloc] initWithAddress:_config.host httpsPort:_config.httpsPort
serverCert:_config.serverCert];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
@@ -67,7 +65,8 @@
return;
}
if (_config.width > 4096 || _config.height > 4096) {
// Only perform this check on GFE (as indicated by MJOLNIR in state value)
if ((_config.width > 4096 || _config.height > 4096) && [serverState containsString:@"MJOLNIR"]) {
// Pascal added support for 8K HEVC encoding support. Maxwell 2 could encode HEVC but only up to 4K.
// We can't directly identify Pascal, but we can look for HEVC Main10 which was added in the same generation.
NSString* codecSupport = [serverInfoResp getStringTag:@"ServerCodecModeSupport"];
@@ -77,6 +76,10 @@
}
}
// Populate the config's version fields from serverinfo
_config.appVersion = appversion;
_config.gfeVersion = gfeVersion;
// resumeApp and launchApp handle calling launchFailed
NSString* sessionUrl;
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
@@ -94,13 +97,9 @@
// Populate RTSP session URL from launch/resume response
_config.rtspSessionUrl = sessionUrl;
// Populate the config's version fields from serverinfo
_config.appVersion = appversion;
_config.gfeVersion = gfeVersion;
// Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks];
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height useFramePacing:self->_config.useFramePacing];
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection];
@@ -114,7 +113,7 @@
- (BOOL) launchApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
HttpResponse* launchResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchRequest:_config]]];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"launch" config:_config]]];
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
if (![launchResp isStatusOk]) {
[_callbacks launchFailed:launchResp.statusMessage];
@@ -132,7 +131,7 @@
- (BOOL) resumeApp:(HttpManager*)hMan receiveSessionUrl:(NSString**)sessionUrl {
HttpResponse* resumeResp = [[HttpResponse alloc] init];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newResumeRequest:_config]]];
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newLaunchOrResumeRequest:@"resume" config:_config]]];
NSString* resume = [resumeResp getStringTag:@"resume"];
if (![resumeResp isStatusOk]) {
[_callbacks launchFailed:resumeResp.statusMessage];
@@ -168,14 +167,26 @@
latencyString = @"N/A";
}
NSString* hostProcessingString;
if (stats.framesWithHostProcessingLatency != 0) {
hostProcessingString = [NSString stringWithFormat:@"\nHost processing latency min/max/avg: %.1f/%.1f/%.1f ms",
stats.minHostProcessingLatency / 10.f,
stats.maxHostProcessingLatency / 10.f,
(float)stats.totalHostProcessingLatency / stats.framesWithHostProcessingLatency / 10.f];
}
else {
hostProcessingString = @"";
}
float interval = stats.endTime - stats.startTime;
return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nFrames dropped by your network connection: %.2f%%\nAverage network latency: %@",
return [NSString stringWithFormat:@"Video stream: %dx%d %.2f FPS (Codec: %@)\nFrames dropped by your network connection: %.2f%%\nAverage network latency: %@%@",
_config.width,
_config.height,
stats.totalFrames / interval,
[_connection getActiveCodecName],
stats.networkDroppedFrames / interval,
latencyString];
latencyString,
hostProcessingString];
}
@end
+8 -7
View File
@@ -10,16 +10,17 @@
#import "ConnectionCallbacks.h"
#include "Limelight.h"
@interface VideoDecoderRenderer : NSObject
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks;
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate;
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
- (void)start;
- (void)stop;
- (void)setHdrMode:(BOOL)enabled;
- (void)cleanup;
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType frameType:(int)frameType pts:(unsigned int)pts;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
@end
+557 -240
View File
@@ -9,20 +9,32 @@
#import "VideoDecoderRenderer.h"
#import "StreamView.h"
#include "Limelight.h"
#include <libavcodec/avcodec.h>
#include <libavcodec/cbs.h>
#include <libavcodec/cbs_av1.h>
#include <libavformat/avio.h>
#include <libavutil/mem.h>
// Private libavformat API for writing the AV1 Codec Configuration Box
extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
int write_seq_header);
@implementation VideoDecoderRenderer {
StreamView* _view;
id<ConnectionCallbacks> _callbacks;
float _streamAspectRatio;
AVSampleBufferDisplayLayer* displayLayer;
Boolean waitingForSps, waitingForPps, waitingForVps;
int videoFormat;
int frameRate;
NSData *spsData, *ppsData, *vpsData;
NSMutableArray *parameterSetBuffers;
NSData *masteringDisplayColorVolume;
NSData *contentLightLevelInfo;
CMVideoFormatDescriptionRef formatDesc;
CADisplayLink* _displayLink;
BOOL framePacing;
}
- (void)reinitializeDisplayLayer
@@ -30,12 +42,23 @@
CALayer *oldLayer = displayLayer;
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
displayLayer.bounds = _view.bounds;
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
// Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
// of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
// respects the PAR encoded in the SPS which causes our computed video-relative
// touch location to be wrong in StreamView if the aspect ratio of the host
// desktop doesn't match the aspect ratio of the stream.
CGSize videoSize;
if (_view.bounds.size.width > _view.bounds.size.height * _streamAspectRatio) {
videoSize = CGSizeMake(_view.bounds.size.height * _streamAspectRatio, _view.bounds.size.height);
} else {
videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
}
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
displayLayer.videoGravity = AVLayerVideoGravityResize;
// 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;
@@ -48,215 +71,442 @@
[_view.layer addSublayer:displayLayer];
}
// We need some parameter sets before we can properly start decoding frames
waitingForSps = true;
spsData = nil;
waitingForPps = true;
ppsData = nil;
waitingForVps = true;
vpsData = nil;
if (formatDesc != nil) {
CFRelease(formatDesc);
formatDesc = nil;
}
}
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing
{
self = [super init];
_view = view;
_callbacks = callbacks;
_streamAspectRatio = aspectRatio;
framePacing = useFramePacing;
parameterSetBuffers = [[NSMutableArray alloc] init];
[self reinitializeDisplayLayer];
return self;
}
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate
{
self->videoFormat = videoFormat;
if (refreshRate > 60) {
// HACK: We seem to just get 60 Hz screen updates even with a 120 FPS stream if
// we don't set preferredFramesPerSecond somewhere. Since we're a UIKit view, we
// have to use CADisplayLink for that. See https://github.com/moonlight-stream/moonlight-ios/issues/372
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
if (@available(iOS 10.0, tvOS 10.0, *)) {
_displayLink.preferredFramesPerSecond = refreshRate;
}
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
}
- (void)displayLinkCallback:(CADisplayLink *)sender
{
// No-op - rendering done in submitDecodeBuffer
self->frameRate = frameRate;
}
- (void)cleanup
- (void)start
{
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
if (@available(iOS 15.0, tvOS 15.0, *)) {
_displayLink.preferredFrameRateRange = CAFrameRateRangeMake(self->frameRate, self->frameRate, self->frameRate);
}
else {
_displayLink.preferredFramesPerSecond = self->frameRate;
}
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
}
// TODO: Refactor this
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
- (void)displayLinkCallback:(CADisplayLink *)sender
{
VIDEO_FRAME_HANDLE handle;
PDECODE_UNIT du;
while (LiPollNextVideoFrame(&handle, &du)) {
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
if (framePacing) {
// Calculate the actual display refresh rate
double displayRefreshRate = 1 / (_displayLink.targetTimestamp - _displayLink.timestamp);
// Only pace frames if the display refresh rate is >= 90% of our stream frame rate.
// Battery saver, accessibility settings, or device thermals can cause the actual
// refresh rate of the display to drop below the physical maximum.
if (displayRefreshRate >= frameRate * 0.9f) {
// Keep one pending frame to smooth out gaps due to
// network jitter at the cost of 1 frame of latency
if (LiGetPendingVideoFrames() == 1) {
break;
}
}
}
}
}
- (void)stop
{
[_displayLink invalidate];
}
#define FRAME_START_PREFIX_SIZE 4
#define NALU_START_PREFIX_SIZE 3
#define NAL_LENGTH_PREFIX_SIZE 4
- (Boolean)readyForPictureData
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
{
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return !waitingForSps && !waitingForPps;
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
// Append a 4 byte buffer to the frame block for the length prefix
status = CMBlockBufferAppendMemoryBlock(frameBuffer, NULL,
NAL_LENGTH_PREFIX_SIZE,
kCFAllocatorDefault, NULL, 0,
NAL_LENGTH_PREFIX_SIZE, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
return;
}
else {
// H.265 requires VPS in addition to SPS and PPS
return !waitingForVps && !waitingForSps && !waitingForPps;
// Write the length prefix to the new buffer
const int dataLength = nalLength - NALU_START_PREFIX_SIZE;
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, frameBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
// Attach the data buffer to the frame buffer by reference
status = CMBlockBufferAppendBufferReference(frameBuffer, dataBuffer, offset + NALU_START_PREFIX_SIZE, dataLength, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
return;
}
}
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
{
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
- (NSData*)getAv1CodecConfigurationBox:(NSData*)frameData {
AVIOContext* ioctx = NULL;
int err;
// If we're at index 1 (first NALU in frame), enqueue this buffer to the memory block
// so it can handle freeing it when the block buffer is destroyed
if (offset == 1) {
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
// Pass the real buffer pointer directly (no offset)
// This will give it to the block buffer to free when it's released.
// All further calls to CMBlockBufferAppendMemoryBlock will do so
// at an offset and will not be asking the buffer to be freed.
status = CMBlockBufferAppendMemoryBlock(existingBuffer, data,
nalLength + 1, // Add 1 for the offset we decremented
kCFAllocatorDefault,
NULL, 0, nalLength + 1, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
// Write the length prefix to existing buffer
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
} else {
// Append a 4 byte buffer to this block for the length prefix
status = CMBlockBufferAppendMemoryBlock(existingBuffer, NULL,
NAL_LENGTH_PREFIX_SIZE,
kCFAllocatorDefault, NULL, 0,
NAL_LENGTH_PREFIX_SIZE, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferAppendMemoryBlock failed: %d", (int)status);
return;
}
// Write the length prefix to the new buffer
int dataLength = nalLength - NALU_START_PREFIX_SIZE;
const uint8_t lengthBytes[] = {(uint8_t)(dataLength >> 24), (uint8_t)(dataLength >> 16),
(uint8_t)(dataLength >> 8), (uint8_t)dataLength};
status = CMBlockBufferReplaceDataBytes(lengthBytes, existingBuffer,
oldOffset, NAL_LENGTH_PREFIX_SIZE);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
// Attach the buffer by reference to the block buffer
status = CMBlockBufferAppendMemoryBlock(existingBuffer, &data[offset+NALU_START_PREFIX_SIZE],
dataLength,
kCFAllocatorNull, // Don't deallocate data on free
NULL, 0, dataLength, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferReplaceDataBytes failed: %d", (int)status);
return;
}
err = avio_open_dyn_buf(&ioctx);
if (err < 0) {
Log(LOG_E, @"avio_open_dyn_buf() failed: %d", err);
return nil;
}
// Submit the IDR frame to write the av1C blob
err = ff_isom_write_av1c(ioctx, (uint8_t*)frameData.bytes, (int)frameData.length, 1);
if (err < 0) {
Log(LOG_E, @"ff_isom_write_av1c() failed: %d", err);
// Fall-through to close and free buffer
}
// Close the dynbuf and get the underlying buffer back (which we must free)
uint8_t* av1cBuf = NULL;
int av1cBufLen = avio_close_dyn_buf(ioctx, &av1cBuf);
Log(LOG_I, @"av1C block is %d bytes", av1cBufLen);
// Only return data if ff_isom_write_av1c() was successful
NSData* data = nil;
if (err >= 0 && av1cBufLen > 0) {
data = [NSData dataWithBytes:av1cBuf length:av1cBufLen];
}
else {
data = nil;
}
av_free(av1cBuf);
return data;
}
// Much of this logic comes from Chrome
- (CMVideoFormatDescriptionRef)createAV1FormatDescriptionForIDRFrame:(NSData*)frameData {
NSMutableDictionary* extensions = [[NSMutableDictionary alloc] init];
CodedBitstreamContext* cbsCtx = NULL;
int err = ff_cbs_init(&cbsCtx, AV_CODEC_ID_AV1, NULL);
if (err < 0) {
Log(LOG_E, @"ff_cbs_init() failed: %d", err);
return nil;
}
AVPacket avPacket = {};
avPacket.data = (uint8_t*)frameData.bytes;
avPacket.size = (int)frameData.length;
// Read the sequence header OBU
CodedBitstreamFragment cbsFrag = {};
err = ff_cbs_read_packet(cbsCtx, &cbsFrag, &avPacket);
if (err < 0) {
Log(LOG_E, @"ff_cbs_read_packet() failed: %d", err);
ff_cbs_close(&cbsCtx);
return nil;
}
#define SET_CFSTR_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (__bridge NSString*)(value)
#define SET_EXTENSION(key, value) extensions[(__bridge NSString*)key] = (value)
SET_EXTENSION(kCMFormatDescriptionExtension_FormatName, @"av01");
// We use the value for YUV without alpha, same as Chrome
// https://developer.apple.com/library/archive/qa/qa1183/_index.html
SET_EXTENSION(kCMFormatDescriptionExtension_Depth, @24);
CodedBitstreamAV1Context* bitstreamCtx = (CodedBitstreamAV1Context*)cbsCtx->priv_data;
AV1RawSequenceHeader* seqHeader = bitstreamCtx->sequence_header;
if (seqHeader == NULL) {
Log(LOG_E, @"AV1 sequence header not found in IDR frame!");
ff_cbs_fragment_free(&cbsFrag);
ff_cbs_close(&cbsCtx);
return nil;
}
switch (seqHeader->color_config.color_primaries) {
case 1: // CP_BT_709
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
kCMFormatDescriptionColorPrimaries_ITU_R_709_2);
break;
case 6: // CP_BT_601
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
kCMFormatDescriptionColorPrimaries_SMPTE_C);
break;
case 9: // CP_BT_2020
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ColorPrimaries,
kCMFormatDescriptionColorPrimaries_ITU_R_2020);
break;
default:
Log(LOG_W, @"Unsupported color_primaries value: %d", seqHeader->color_config.color_primaries);
break;
}
switch (seqHeader->color_config.transfer_characteristics) {
case 1: // TC_BT_709
case 6: // TC_BT_601
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_ITU_R_709_2);
break;
case 7: // TC_SMPTE_240
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_SMPTE_240M_1995);
break;
case 8: // TC_LINEAR
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_Linear);
break;
case 14: // TC_BT_2020_10_BIT
case 15: // TC_BT_2020_12_BIT
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_ITU_R_2020);
break;
case 16: // TC_SMPTE_2084
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_SMPTE_ST_2084_PQ);
break;
case 17: // TC_HLG
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_TransferFunction,
kCMFormatDescriptionTransferFunction_ITU_R_2100_HLG);
break;
default:
Log(LOG_W, @"Unsupported transfer_characteristics value: %d", seqHeader->color_config.transfer_characteristics);
break;
}
switch (seqHeader->color_config.matrix_coefficients) {
case 1: // MC_BT_709
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
kCMFormatDescriptionYCbCrMatrix_ITU_R_709_2);
break;
case 6: // MC_BT_601
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
kCMFormatDescriptionYCbCrMatrix_ITU_R_601_4);
break;
case 7: // MC_SMPTE_240
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
kCMFormatDescriptionYCbCrMatrix_SMPTE_240M_1995);
break;
case 9: // MC_BT_2020_NCL
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_YCbCrMatrix,
kCMFormatDescriptionYCbCrMatrix_ITU_R_2020);
break;
default:
Log(LOG_W, @"Unsupported matrix_coefficients value: %d", seqHeader->color_config.matrix_coefficients);
break;
}
SET_EXTENSION(kCMFormatDescriptionExtension_FullRangeVideo, @(seqHeader->color_config.color_range == 1));
// Progressive content
SET_EXTENSION(kCMFormatDescriptionExtension_FieldCount, @(1));
switch (seqHeader->color_config.chroma_sample_position) {
case 1: // CSP_VERTICAL
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
kCMFormatDescriptionChromaLocation_Left);
break;
case 2: // CSP_COLOCATED
SET_CFSTR_EXTENSION(kCMFormatDescriptionExtension_ChromaLocationTopField,
kCMFormatDescriptionChromaLocation_TopLeft);
break;
default:
Log(LOG_W, @"Unsupported chroma_sample_position value: %d", seqHeader->color_config.chroma_sample_position);
break;
}
if (contentLightLevelInfo) {
SET_EXTENSION(kCMFormatDescriptionExtension_ContentLightLevelInfo, contentLightLevelInfo);
}
if (masteringDisplayColorVolume) {
SET_EXTENSION(kCMFormatDescriptionExtension_MasteringDisplayColorVolume, masteringDisplayColorVolume);
}
// Referenced the VP9 code in Chrome that performs a similar function
// https://source.chromium.org/chromium/chromium/src/+/main:media/gpu/mac/vt_config_util.mm;drc=977dc02c431b4979e34c7792bc3d646f649dacb4;l=155
extensions[(__bridge NSString*)kCMFormatDescriptionExtension_SampleDescriptionExtensionAtoms] =
@{
@"av1C" : [self getAv1CodecConfigurationBox:frameData],
};
extensions[@"BitsPerComponent"] = @(bitstreamCtx->bit_depth);
#undef SET_EXTENSION
#undef SET_CFSTR_EXTENSION
// AV1 doesn't have a special format description function like H.264 and HEVC have, so we just use the generic one
CMVideoFormatDescriptionRef formatDesc = NULL;
OSStatus status = CMVideoFormatDescriptionCreate(kCFAllocatorDefault, kCMVideoCodecType_AV1,
bitstreamCtx->frame_width, bitstreamCtx->frame_height,
(__bridge CFDictionaryRef)extensions,
&formatDesc);
if (status != noErr) {
Log(LOG_E, @"Failed to create AV1 format description: %d", (int)status);
formatDesc = NULL;
}
ff_cbs_fragment_free(&cbsFrag);
ff_cbs_close(&cbsCtx);
return formatDesc;
}
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType frameType:(int)frameType pts:(unsigned int)pts
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
{
OSStatus status;
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;
// We got a new VPS so wait for a new SPS to match it
waitingForSps = true;
}
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 (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;
}
// See if we've got all the parameter sets we need for our video format
if ([self readyForPictureData]) {
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
Log(LOG_I, @"Constructing new H264 format description");
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
2, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
&formatDesc);
if (status != noErr) {
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
formatDesc = NULL;
}
}
else {
const uint8_t* const parameterSetPointers[] = { [vpsData bytes], [spsData bytes], [ppsData bytes] };
const size_t parameterSetSizes[] = { [vpsData length], [spsData length], [ppsData length] };
Log(LOG_I, @"Constructing new HEVC format description");
if (@available(iOS 11.0, *)) {
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
3, /* count of parameter sets */
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
nil,
&formatDesc);
} else {
// This means Moonlight-common-c decided to give us an HEVC stream
// even though we said we couldn't support it. All we can do is abort().
abort();
}
if (status != noErr) {
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
formatDesc = NULL;
}
// Construct a new format description object each time we receive an IDR frame
if (du->frameType == FRAME_TYPE_IDR) {
if (bufferType != BUFFER_TYPE_PICDATA) {
if (bufferType == BUFFER_TYPE_VPS || bufferType == BUFFER_TYPE_SPS || bufferType == BUFFER_TYPE_PPS) {
// Add new parameter set into the parameter set array
int startLen = data[2] == 0x01 ? 3 : 4;
[parameterSetBuffers addObject:[NSData dataWithBytes:&data[startLen] length:length - startLen]];
}
// 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;
}
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
// Create the new format description when we get the first picture data buffer of an IDR frame.
// This is the only way we know that there is no more CSD for this frame.
//
// NB: This logic depends on the fact that we submit all picture data in one buffer!
// No frame data to submit for these NALUs
return DR_OK;
// Free the old format description
if (formatDesc != NULL) {
CFRelease(formatDesc);
formatDesc = NULL;
}
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
// Construct parameter set arrays for the format description
size_t parameterSetCount = [parameterSetBuffers count];
const uint8_t* parameterSetPointers[parameterSetCount];
size_t parameterSetSizes[parameterSetCount];
for (int i = 0; i < parameterSetCount; i++) {
NSData* parameterSet = parameterSetBuffers[i];
parameterSetPointers[i] = parameterSet.bytes;
parameterSetSizes[i] = parameterSet.length;
}
Log(LOG_I, @"Constructing new H264 format description");
status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault,
parameterSetCount,
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
&formatDesc);
if (status != noErr) {
Log(LOG_E, @"Failed to create H264 format description: %d", (int)status);
formatDesc = NULL;
}
// Free parameter set buffers after submission
[parameterSetBuffers removeAllObjects];
}
else if (videoFormat & VIDEO_FORMAT_MASK_H265) {
// Construct parameter set arrays for the format description
size_t parameterSetCount = [parameterSetBuffers count];
const uint8_t* parameterSetPointers[parameterSetCount];
size_t parameterSetSizes[parameterSetCount];
for (int i = 0; i < parameterSetCount; i++) {
NSData* parameterSet = parameterSetBuffers[i];
parameterSetPointers[i] = parameterSet.bytes;
parameterSetSizes[i] = parameterSet.length;
}
Log(LOG_I, @"Constructing new HEVC format description");
NSMutableDictionary* videoFormatParams = [[NSMutableDictionary alloc] init];
if (contentLightLevelInfo) {
[videoFormatParams setObject:contentLightLevelInfo forKey:(__bridge NSString*)kCMFormatDescriptionExtension_ContentLightLevelInfo];
}
if (masteringDisplayColorVolume) {
[videoFormatParams setObject:masteringDisplayColorVolume forKey:(__bridge NSString*)kCMFormatDescriptionExtension_MasteringDisplayColorVolume];
}
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
parameterSetCount,
parameterSetPointers,
parameterSetSizes,
NAL_LENGTH_PREFIX_SIZE,
(__bridge CFDictionaryRef)videoFormatParams,
&formatDesc);
if (status != noErr) {
Log(LOG_E, @"Failed to create HEVC format description: %d", (int)status);
formatDesc = NULL;
}
// Free parameter set buffers after submission
[parameterSetBuffers removeAllObjects];
}
else if (videoFormat & VIDEO_FORMAT_MASK_AV1) {
NSData* fullFrameData = [NSData dataWithBytesNoCopy:data length:length freeWhenDone:NO];
Log(LOG_I, @"Constructing new AV1 format description");
formatDesc = [self createAV1FormatDescriptionForIDRFrame:fullFrameData];
}
else {
// Unsupported codec!
abort();
}
}
if (formatDesc == NULL) {
@@ -269,13 +519,9 @@
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];
});
// Recreate the display layer. We are already on the main thread,
// so this is safe to do right here.
[self reinitializeDisplayLayer];
// Request an IDR frame to initialize the new decoder
free(data);
@@ -283,85 +529,156 @@
}
// Now we're decoding actual frame data here
CMBlockBufferRef blockBuffer;
CMBlockBufferRef frameBlockBuffer;
CMBlockBufferRef dataBlockBuffer;
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &blockBuffer);
status = CMBlockBufferCreateWithMemoryBlock(NULL, data, length, kCFAllocatorDefault, NULL, 0, length, 0, &dataBlockBuffer);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
Log(LOG_E, @"CMBlockBufferCreateWithMemoryBlock failed: %d", (int)status);
free(data);
return DR_NEED_IDR;
}
int lastOffset = -1;
for (int i = 0; i < length - FRAME_START_PREFIX_SIZE; i++) {
// Search for a NALU
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
// It's the start of a new NALU
if (lastOffset != -1) {
// We've seen a start before this so enqueue that NALU
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:i - lastOffset];
}
lastOffset = i;
}
}
if (lastOffset != -1) {
// Enqueue the remaining data
[self updateBufferForRange:blockBuffer data:data offset:lastOffset length:length - lastOffset];
}
// From now on, CMBlockBuffer owns the data pointer and will free it when it's dereferenced
CMSampleBufferRef sampleBuffer;
status = CMSampleBufferCreate(kCFAllocatorDefault,
blockBuffer,
true, NULL,
NULL, formatDesc, 1, 0,
NULL, 0, NULL,
&sampleBuffer);
status = CMBlockBufferCreateEmpty(NULL, 0, 0, &frameBlockBuffer);
if (status != noErr) {
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
CFRelease(blockBuffer);
Log(LOG_E, @"CMBlockBufferCreateEmpty failed: %d", (int)status);
CFRelease(dataBlockBuffer);
return DR_NEED_IDR;
}
CFArrayRef attachments = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, YES);
CFMutableDictionaryRef dict = (CFMutableDictionaryRef)CFArrayGetValueAtIndex(attachments, 0);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
if (frameType == FRAME_TYPE_PFRAME) {
// P-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
} else {
// I-frame
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanFalse);
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
}
// Enqueue video samples on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
// Enqueue the next frame
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
if (frameType == FRAME_TYPE_IDR) {
// Ensure the layer is visible now
self->displayLayer.hidden = NO;
// Tell our parent VC to hide the progress indicator
[self->_callbacks videoContentShown];
// H.264 and HEVC formats require NAL prefix fixups from Annex B to length-delimited
if (videoFormat & (VIDEO_FORMAT_MASK_H264 | VIDEO_FORMAT_MASK_H265)) {
int lastOffset = -1;
for (int i = 0; i < length - NALU_START_PREFIX_SIZE; i++) {
// Search for a NALU
if (data[i] == 0 && data[i+1] == 0 && data[i+2] == 1) {
// It's the start of a new NALU
if (lastOffset != -1) {
// We've seen a start before this so enqueue that NALU
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:i - lastOffset];
}
lastOffset = i;
}
}
// Dereference the buffers
CFRelease(blockBuffer);
CFRelease(sampleBuffer);
});
if (lastOffset != -1) {
// Enqueue the remaining data
[self updateAnnexBBufferForRange:frameBlockBuffer dataBlock:dataBlockBuffer offset:lastOffset length:length - lastOffset];
}
}
else {
// For formats that require no length-changing fixups, just append a reference to the raw data block
status = CMBlockBufferAppendBufferReference(frameBlockBuffer, dataBlockBuffer, 0, length, 0);
if (status != noErr) {
Log(LOG_E, @"CMBlockBufferAppendBufferReference failed: %d", (int)status);
return DR_NEED_IDR;
}
}
CMSampleBufferRef sampleBuffer;
CMSampleTimingInfo sampleTiming = {kCMTimeInvalid, CMTimeMake(du->presentationTimeUs, 1000000), kCMTimeInvalid};
status = CMSampleBufferCreateReady(kCFAllocatorDefault,
frameBlockBuffer,
formatDesc, 1, 1,
&sampleTiming, 0, NULL,
&sampleBuffer);
if (status != noErr) {
Log(LOG_E, @"CMSampleBufferCreate failed: %d", (int)status);
CFRelease(dataBlockBuffer);
CFRelease(frameBlockBuffer);
return DR_NEED_IDR;
}
// Enqueue the next frame
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
if (du->frameType == FRAME_TYPE_IDR) {
// Ensure the layer is visible now
self->displayLayer.hidden = NO;
// Tell our parent VC to hide the progress indicator
[self->_callbacks videoContentShown];
}
// Dereference the buffers
CFRelease(dataBlockBuffer);
CFRelease(frameBlockBuffer);
CFRelease(sampleBuffer);
return DR_OK;
}
- (void)setHdrMode:(BOOL)enabled {
SS_HDR_METADATA hdrMetadata;
BOOL hasMetadata = enabled && LiGetHdrMetadata(&hdrMetadata);
BOOL metadataChanged = NO;
if (hasMetadata && hdrMetadata.displayPrimaries[0].x != 0 && hdrMetadata.maxDisplayLuminance != 0) {
// This data is all in big-endian
struct {
vector_ushort2 primaries[3];
vector_ushort2 white_point;
uint32_t luminance_max;
uint32_t luminance_min;
} __attribute__((packed, aligned(4))) mdcv;
// mdcv is in GBR order while SS_HDR_METADATA is in RGB order
mdcv.primaries[0].x = __builtin_bswap16(hdrMetadata.displayPrimaries[1].x);
mdcv.primaries[0].y = __builtin_bswap16(hdrMetadata.displayPrimaries[1].y);
mdcv.primaries[1].x = __builtin_bswap16(hdrMetadata.displayPrimaries[2].x);
mdcv.primaries[1].y = __builtin_bswap16(hdrMetadata.displayPrimaries[2].y);
mdcv.primaries[2].x = __builtin_bswap16(hdrMetadata.displayPrimaries[0].x);
mdcv.primaries[2].y = __builtin_bswap16(hdrMetadata.displayPrimaries[0].y);
mdcv.white_point.x = __builtin_bswap16(hdrMetadata.whitePoint.x);
mdcv.white_point.y = __builtin_bswap16(hdrMetadata.whitePoint.y);
// These luminance values are in 10000ths of a nit
mdcv.luminance_max = __builtin_bswap32((uint32_t)hdrMetadata.maxDisplayLuminance * 10000);
mdcv.luminance_min = __builtin_bswap32(hdrMetadata.minDisplayLuminance);
NSData* newMdcv = [NSData dataWithBytes:&mdcv length:sizeof(mdcv)];
if (masteringDisplayColorVolume == nil || ![newMdcv isEqualToData:masteringDisplayColorVolume]) {
masteringDisplayColorVolume = newMdcv;
metadataChanged = YES;
}
}
else if (masteringDisplayColorVolume != nil) {
masteringDisplayColorVolume = nil;
metadataChanged = YES;
}
if (hasMetadata && hdrMetadata.maxContentLightLevel != 0 && hdrMetadata.maxFrameAverageLightLevel != 0) {
// This data is all in big-endian
struct {
uint16_t max_content_light_level;
uint16_t max_frame_average_light_level;
} __attribute__((packed, aligned(2))) cll;
cll.max_content_light_level = __builtin_bswap16(hdrMetadata.maxContentLightLevel);
cll.max_frame_average_light_level = __builtin_bswap16(hdrMetadata.maxFrameAverageLightLevel);
NSData* newCll = [NSData dataWithBytes:&cll length:sizeof(cll)];
if (contentLightLevelInfo == nil || ![newCll isEqualToData:contentLightLevelInfo]) {
contentLightLevelInfo = newCll;
metadataChanged = YES;
}
}
else if (contentLightLevelInfo != nil) {
contentLightLevelInfo = nil;
metadataChanged = YES;
}
// If the metadata changed, request an IDR frame to re-create the CMVideoFormatDescription
if (metadataChanged) {
LiRequestIdrFrame();
}
}
@end
+8
View File
@@ -27,6 +27,14 @@ FOUNDATION_EXPORT NSString *const deviceName;
+ (NSData*) hexToBytes:(NSString*) hex;
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
+ (BOOL) isActiveNetworkVPN;
+ (BOOL) parseAddressPortString:(NSString*)addressPort address:(NSRange*)address port:(NSRange*)port;
+ (NSString*) addressPortStringToAddress:(NSString*)addressPort;
+ (unsigned short) addressPortStringToPort:(NSString*)addressPort;
+ (NSString*) addressAndPortToAddressPortString:(NSString*)address port:(unsigned short)port;
#if !TARGET_OS_TV
+ (void) launchUrl:(NSString*)urlString;
#endif
@end
+74 -1
View File
@@ -64,15 +64,88 @@ NSString *const deviceName = @"roth";
return NO;
}
#if !TARGET_OS_TV
+ (void) launchUrl:(NSString*)urlString {
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:urlString] options:@{} completionHandler:nil];
}
#endif
+ (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"]];
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
}]];
#endif
}
+ (BOOL) parseAddressPortString:(NSString*)addressPort address:(NSRange*)address port:(NSRange*)port {
if (![addressPort containsString:@":"]) {
// If there's no port or IPv6 separator, the whole thing is an address
*address = NSMakeRange(0, [addressPort length]);
*port = NSMakeRange(NSNotFound, 0);
return TRUE;
}
NSInteger locationOfOpeningBracket = [addressPort rangeOfString:@"["].location;
NSInteger locationOfClosingBracket = [addressPort rangeOfString:@"]"].location;
if (locationOfOpeningBracket != NSNotFound || locationOfClosingBracket != NSNotFound) {
// If we have brackets, it's an IPv6 address
if (locationOfOpeningBracket == NSNotFound || locationOfClosingBracket == NSNotFound ||
locationOfClosingBracket < locationOfOpeningBracket) {
// Invalid address format
return FALSE;
}
// Cut at the brackets
*address = NSMakeRange(locationOfOpeningBracket + 1, locationOfClosingBracket - locationOfOpeningBracket - 1);
}
else {
// It's an IPv4 address, so just cut at the port separator
*address = NSMakeRange(0, [addressPort rangeOfString:@":"].location);
}
NSUInteger remainingStringLocation = address->location + address->length;
NSRange remainingStringRange = NSMakeRange(remainingStringLocation, [addressPort length] - remainingStringLocation);
NSInteger locationOfPortSeparator = [addressPort rangeOfString:@":" options:0 range:remainingStringRange].location;
if (locationOfPortSeparator != NSNotFound) {
*port = NSMakeRange(locationOfPortSeparator + 1, [addressPort length] - locationOfPortSeparator - 1);
}
else {
*port = NSMakeRange(NSNotFound, 0);
}
return TRUE;
}
+ (NSString*) addressPortStringToAddress:(NSString*)addressPort {
NSRange addressRange, portRange;
if (![self parseAddressPortString:addressPort address:&addressRange port:&portRange]) {
return nil;
}
return [addressPort substringWithRange:addressRange];
}
+ (unsigned short) addressPortStringToPort:(NSString*)addressPort {
NSRange addressRange, portRange;
if (![self parseAddressPortString:addressPort address:&addressRange port:&portRange] || portRange.location == NSNotFound) {
return 47989;
}
return [[addressPort substringWithRange:portRange] integerValue];
}
+ (NSString*) addressAndPortToAddressPortString:(NSString*)address port:(unsigned short)port {
if ([address containsString:@":"]) {
// IPv6 addresses require escaping
return [NSString stringWithFormat:@"[%@]:%u", address, port];
}
else {
return [NSString stringWithFormat:@"%@:%u", address, port];
}
}
@end
@implementation NSString (NSStringWithTrim)
@@ -64,7 +64,7 @@ static NSMutableSet* hostList;
// failure callback could be invoked.
dispatch_sync(dispatch_get_main_queue(), ^{
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@\n\nIf your host PC is running Sunshine, navigate to the Sunshine web UI to enter the PIN.", PIN]
preferredStyle:UIAlertControllerStyleAlert];
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
self->_pairAlert = nil;
@@ -168,7 +168,7 @@ static NSMutableSet* hostList;
// Exempt this host from discovery while handling the applist query
[self->_discMan pauseDiscoveryForHost:host];
AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress serverCert:host.serverCert uniqueID:self->_uniqueId];
AppListResponse* appListResp = [ConnectionHelper getAppListForHost:host];
[self->_discMan resumeDiscoveryForHost:host];
@@ -373,7 +373,7 @@ static NSMutableSet* hostList;
return;
}
HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId serverCert:host.serverCert];
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
// Exempt this host from discovery while handling the serverinfo request
@@ -492,6 +492,14 @@ static NSMutableSet* hostList;
self->_showHiddenApps = YES;
[self hostClicked:host view:view];
}]];
#if !TARGET_OS_TV
if (host.isNvidiaServerSoftware) {
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
}]];
}
#endif
}
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
[self showLoadingFrame:^{
@@ -524,8 +532,11 @@ static NSMutableSet* hostList;
}]];
#if !TARGET_OS_TV
if (host.state != StateOnline) {
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"NVIDIA GameStream End-of-Service" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/NVIDIA-GameStream-End-Of-Service-Announcement-FAQ"];
}]];
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
[Utils launchUrl:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"];
}]];
}
#endif
@@ -593,6 +604,7 @@ static NSMutableSet* hostList;
- (void) prepareToStreamApp:(TemporaryApp *)app {
_streamConfig = [[StreamConfiguration alloc] init];
_streamConfig.host = app.host.activeAddress;
_streamConfig.httpsPort = app.host.httpsPort;
_streamConfig.appID = app.id;
_streamConfig.appName = app.name;
_streamConfig.serverCert = app.host.serverCert;
@@ -625,39 +637,69 @@ static NSMutableSet* hostList;
_streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
_streamConfig.allowHevc = streamSettings.useHevc;
_streamConfig.useFramePacing = streamSettings.useFramePacing;
_streamConfig.swapABXYButtons = streamSettings.swapABXYButtons;
// multiController must be set before calling getConnectedGamepadMask
_streamConfig.multiController = streamSettings.multiController;
_streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig];
// Probe for supported channel configurations
long outputChannels = [AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
Log(LOG_I, @"Audio device supports %d channels", outputChannels);
if (outputChannels >= 8) {
int physicalOutputChannels = (int)[AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
Log(LOG_I, @"Audio device supports %d channels", physicalOutputChannels);
int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
if (numberOfChannels >= 8) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
}
else if (outputChannels >= 6) {
else if (numberOfChannels >= 6) {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
}
else {
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
}
// HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client.
// It additionally requires an HEVC Main10 encoder on the server (GTX 1000+).
//
// It should also be a user preference, since some games may require higher peak
// brightness than the iOS device can support to look correct in HDR mode.
if (@available(iOS 11.3, *)) {
_streamConfig.enableHdr =
app.hdrSupported && // App supported
(app.host.serverCodecModeSupport & 0x200) != 0 && // HEVC Main10 encoding on host PC GPU
VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && // Decoder supported
(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0 && // Display supported
streamSettings.enableHdr; // User wants it enabled
_streamConfig.serverCodecModeSupport = app.host.serverCodecModeSupport;
switch (streamSettings.preferredCodec) {
case CODEC_PREF_AV1:
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1)) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN8;
}
#endif
// Fall-through
case CODEC_PREF_AUTO:
case CODEC_PREF_HEVC:
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
}
// Fall-through
case CODEC_PREF_H264:
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H264;
break;
}
// HEVC is supported if the user wants it (or it's required by the chosen resolution) and the SoC supports it
if ((_streamConfig.width > 4096 || _streamConfig.height > 4096 || streamSettings.enableHdr) && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265;
// HEVC Main10 is supported if the user wants it and the display supports it
if (streamSettings.enableHdr && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_H265_MAIN10;
}
}
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
// Add the AV1 Main10 format if AV1 and HDR are both enabled and supported
if ((_streamConfig.supportedVideoFormats & VIDEO_FORMAT_MASK_AV1) && streamSettings.enableHdr &&
VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1) && (AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0) {
_streamConfig.supportedVideoFormats |= VIDEO_FORMAT_AV1_MAIN10;
}
#endif
}
- (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
@@ -715,7 +757,7 @@ static NSMutableSet* hostList;
Log(LOG_I, @"Quitting application: %@", currentApp.name);
[self showLoadingFrame: ^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host.activeAddress uniqueId:self->_uniqueId serverCert:app.host.serverCert];
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host];
HttpResponse* quitResponse = [[HttpResponse alloc] init];
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
@@ -626,10 +626,10 @@ const int FrontViewPositionNone = 0xff;
_frontViewPosition = FrontViewPositionLeft;
_rearViewPosition = FrontViewPositionLeft;
_rightViewPosition = FrontViewPositionLeft;
_rearViewRevealWidth = 260.0f;
_rearViewRevealWidth = 510.0f;
_rearViewRevealOverdraw = 60.0f;
_rearViewRevealDisplacement = 40.0f;
_rightViewRevealWidth = 260.0f;
_rightViewRevealWidth = 510.0f;
_rightViewRevealOverdraw = 60.0f;
_rightViewRevealDisplacement = 40.0f;
_bounceBackOnOverdraw = YES;
@@ -14,12 +14,16 @@
@property (strong, nonatomic) IBOutlet UISlider *bitrateSlider;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
@property (strong, nonatomic) IBOutlet UIView *resolutionDisplayView;
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *swapABXYButtonsSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hevcSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
@@ -11,9 +11,11 @@
#import "DataManager.h"
#import <VideoToolbox/VideoToolbox.h>
#import <AVFoundation/AVFoundation.h>
@implementation SettingsViewController {
NSInteger _bitrate;
NSInteger _lastSelectedResolutionIndex;
}
@dynamic overrideUserInterfaceStyle;
@@ -48,6 +50,10 @@ static const int bitrateTable[] = {
150000,
};
const int RESOLUTION_TABLE_SIZE = 7;
const int RESOLUTION_TABLE_CUSTOM_INDEX = RESOLUTION_TABLE_SIZE - 1;
CGSize resolutionTable[RESOLUTION_TABLE_SIZE];
-(int)getSliderValueForBitrate:(NSInteger)bitrate {
int i;
@@ -61,35 +67,33 @@ static const int bitrateTable[] = {
return i - 1;
}
// This view is rooted at a ScrollView. To make it scrollable,
// we'll update content size here.
-(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 child views
// which represent the horizontal and vertical scrolling
// indicators. Ignore any views we don't recognize.
if (![view isKindOfClass:[UILabel class]] &&
![view isKindOfClass:[UISegmentedControl class]] &&
![view isKindOfClass:[UISlider class]]) {
continue;
}
CGFloat currentViewY = view.frame.origin.y + view.frame.size.height;
if (currentViewY > highestViewY) {
highestViewY = currentViewY;
}
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 child views
// which represent the horizontal and vertical scrolling
// indicators. Ignore any views we don't recognize.
if (![view isKindOfClass:[UILabel class]] &&
![view isKindOfClass:[UISegmentedControl class]] &&
![view isKindOfClass:[UISlider class]]) {
continue;
}
// 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);
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.
@@ -108,6 +112,20 @@ static const int bitrateTable[] = {
}
}
BOOL isCustomResolution(CGSize res) {
if (res.width == 0 && res.height == 0) {
return NO;
}
for (int i = 0; i < RESOLUTION_TABLE_CUSTOM_INDEX; i++) {
if (res.width == resolutionTable[i].width && res.height == resolutionTable[i].height) {
return NO;
}
}
return YES;
}
- (void)viewDidLoad {
[super viewDidLoad];
@@ -121,6 +139,31 @@ static const int bitrateTable[] = {
// Ensure we pick a bitrate that falls exactly onto a slider notch
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
// Get the size of the screen with and without safe area insets
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
CGFloat screenScale = window.screen.scale;
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
CGFloat fullScreenWidth = window.frame.size.width * screenScale;
CGFloat fullScreenHeight = window.frame.size.height * screenScale;
self.resolutionDisplayView.layer.cornerRadius = 10;
self.resolutionDisplayView.clipsToBounds = YES;
UITapGestureRecognizer *resolutionDisplayViewTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(resolutionDisplayViewTapped:)];
[self.resolutionDisplayView addGestureRecognizer:resolutionDisplayViewTap];
resolutionTable[0] = CGSizeMake(640, 360);
resolutionTable[1] = CGSizeMake(1280, 720);
resolutionTable[2] = CGSizeMake(1920, 1080);
resolutionTable[3] = CGSizeMake(3840, 2160);
resolutionTable[4] = CGSizeMake(safeAreaWidth, fullScreenHeight);
resolutionTable[5] = CGSizeMake(fullScreenWidth, fullScreenHeight);
resolutionTable[6] = CGSizeMake([currentSettings.width integerValue], [currentSettings.height integerValue]); // custom initial value
// Don't populate the custom entry unless we have a custom resolution
if (!isCustomResolution(resolutionTable[6])) {
resolutionTable[6] = CGSizeMake(0, 0);
}
NSInteger framerate;
switch ([currentSettings.framerate integerValue]) {
@@ -135,23 +178,16 @@ static const int bitrateTable[] = {
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;
case 2160:
resolution = 3;
NSInteger resolution = 1;
for (int i = 0; i < RESOLUTION_TABLE_SIZE; i++) {
if ((int) resolutionTable[i].height == [currentSettings.height intValue]
&& (int) resolutionTable[i].width == [currentSettings.width intValue]) {
resolution = i;
break;
}
}
// Only show the 120 FPS option if we have a > 60-ish Hz display
bool enable120Fps = false;
if (@available(iOS 10.3, tvOS 10.3, *)) {
@@ -162,35 +198,46 @@ static const int bitrateTable[] = {
if (!enable120Fps) {
[self.framerateSelector removeSegmentAtIndex:2 animated:NO];
}
// Only show the 4K option for "recent" devices. We'll judge that by whether
// they support HEVC decoding (A9 or later).
if (@available(iOS 11.0, tvOS 11.0, *)) {
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
[self.resolutionSelector removeSegmentAtIndex:3 animated:NO];
}
// Disable codec selector segments for unsupported codecs
#if defined(__IPHONE_16_0) || defined(__TVOS_16_0)
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_AV1))
#endif
{
[self.codecSelector removeSegmentAtIndex:2 animated:NO];
}
else {
[self.resolutionSelector removeSegmentAtIndex:3 animated:NO];
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
[self.codecSelector removeSegmentAtIndex:1 animated:NO];
// Only enable the 4K option for "recent" devices. We'll judge that by whether
// they support HEVC decoding (A9 or later).
[self.resolutionSelector setEnabled:NO forSegmentAtIndex:3];
}
switch (currentSettings.preferredCodec) {
case CODEC_PREF_AUTO:
[self.codecSelector setSelectedSegmentIndex:self.codecSelector.numberOfSegments - 1];
break;
case CODEC_PREF_AV1:
[self.codecSelector setSelectedSegmentIndex:2];
break;
case CODEC_PREF_HEVC:
[self.codecSelector setSelectedSegmentIndex:1];
break;
case CODEC_PREF_H264:
[self.codecSelector setSelectedSegmentIndex:0];
break;
}
// Disable the HEVC selector if HEVC is not supported by the hardware
// or the version of iOS. See comment in Connection.m for reasoning behind
// the iOS 11.3 check.
if (@available(iOS 11.3, tvOS 11.3, *)) {
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
[self.hevcSelector setSelectedSegmentIndex:currentSettings.useHevc ? 1 : 0];
}
else {
[self.hevcSelector removeAllSegments];
[self.hevcSelector insertSegmentWithTitle:@"Unsupported on this device" atIndex:0 animated:NO];
[self.hevcSelector setEnabled:NO];
}
if (!VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || !(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10)) {
[self.hdrSelector removeAllSegments];
[self.hdrSelector insertSegmentWithTitle:@"Unsupported on this device" atIndex:0 animated:NO];
[self.hdrSelector setEnabled:NO];
}
else {
[self.hevcSelector removeAllSegments];
[self.hevcSelector insertSegmentWithTitle:@"Requires iOS 11.3 or later" atIndex:0 animated:NO];
[self.hevcSelector setEnabled:NO];
[self.hdrSelector setSelectedSegmentIndex:currentSettings.enableHdr ? 1 : 0];
}
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
@@ -198,13 +245,16 @@ static const int bitrateTable[] = {
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
[self.framePacingSelector setSelectedSegmentIndex:currentSettings.useFramePacing ? 1 : 0];
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
[self.swapABXYButtonsSelector setSelectedSegmentIndex:currentSettings.swapABXYButtons ? 1 : 0];
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
_lastSelectedResolutionIndex = resolution;
[self.resolutionSelector setSelectedSegmentIndex:resolution];
[self.resolutionSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.resolutionSelector addTarget:self action:@selector(newResolutionChosen) forControlEvents:UIControlEventValueChanged];
[self.framerateSelector setSelectedSegmentIndex:framerate];
[self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
[self.framerateSelector addTarget:self action:@selector(updateBitrate) forControlEvents:UIControlEventValueChanged];
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
[self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
[self.bitrateSlider setMinimumValue:0];
@@ -212,6 +262,7 @@ static const int bitrateTable[] = {
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
[self updateBitrateText];
[self updateResolutionDisplayViewText];
}
- (void) touchModeChanged {
@@ -219,45 +270,192 @@ static const int bitrateTable[] = {
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0];
}
- (void) newResolutionFpsChosen {
- (void) updateBitrate {
NSInteger fps = [self getChosenFrameRate];
NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight];
NSInteger defaultBitrate;
// 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);
// Don't scale bitrate linearly beyond 60 FPS. It's definitely not a linear
// bitrate increase for frame rate once we get to values that high.
float frameRateFactor = (fps <= 60 ? fps : (sqrtf(fps / 60.f) * 60.f)) / 30.f;
// TODO: Collect some empirical data to see if these defaults make sense.
// We're just using the values that the Shield used, as we have for years.
struct {
NSInteger pixels;
int factor;
} resTable[] = {
{ 640 * 360, 1 },
{ 854 * 480, 2 },
{ 1280 * 720, 5 },
{ 1920 * 1080, 10 },
{ 2560 * 1440, 20 },
{ 3840 * 2160, 40 },
{ -1, -1 }
};
// Calculate the resolution factor by linear interpolation of the resolution table
float resolutionFactor;
NSInteger pixels = width * height;
for (int i = 0;; i++) {
if (pixels == resTable[i].pixels) {
// We can bail immediately for exact matches
resolutionFactor = resTable[i].factor;
break;
}
else if (pixels < resTable[i].pixels) {
if (i == 0) {
// Never go below the lowest resolution entry
resolutionFactor = resTable[i].factor;
}
else {
// Interpolate between the entry greater than the chosen resolution (i) and the entry less than the chosen resolution (i-1)
resolutionFactor = ((float)(pixels - resTable[i-1].pixels) / (resTable[i].pixels - resTable[i-1].pixels)) * (resTable[i].factor - resTable[i-1].factor) + resTable[i-1].factor;
}
break;
}
else if (resTable[i].pixels == -1) {
// Never go above the highest resolution entry
resolutionFactor = resTable[i-1].factor;
break;
}
}
// This covers 1280x720 and 1280x800 too
else if (width * height <= 1366 * 768) {
defaultBitrate = 5000 * (fps / 30.0);
}
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
defaultBitrate = round(resolutionFactor * frameRateFactor) * 1000;
_bitrate = MIN(defaultBitrate, 100000);
assert(bitrateTable[[self getSliderValueForBitrate:_bitrate]] == _bitrate);
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
[self updateBitrateText];
}
- (void) newResolutionChosen {
BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
if (lastSegmentSelected) {
[self promptCustomResolutionDialog];
}
else {
[self updateBitrate];
[self updateResolutionDisplayViewText];
_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
}
}
- (void) promptCustomResolutionDialog {
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Enter Custom Resolution" message:nil preferredStyle:UIAlertControllerStyleAlert];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"Video Width";
textField.clearButtonMode = UITextFieldViewModeAlways;
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.keyboardType = UIKeyboardTypeNumberPad;
if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width == 0) {
textField.text = @"";
}
else {
textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width];
}
}];
[alertController addTextFieldWithConfigurationHandler:^(UITextField *textField) {
textField.placeholder = @"Video Height";
textField.clearButtonMode = UITextFieldViewModeAlways;
textField.borderStyle = UITextBorderStyleRoundedRect;
textField.keyboardType = UIKeyboardTypeNumberPad;
if (resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height == 0) {
textField.text = @"";
}
else {
textField.text = [NSString stringWithFormat:@"%d", (int) resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height];
}
}];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction *action) {
NSArray * textfields = alertController.textFields;
UITextField *widthField = textfields[0];
UITextField *heightField = textfields[1];
long width = [widthField.text integerValue];
long height = [heightField.text integerValue];
if (width <= 0 || height <= 0) {
// Restore the previous selection
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
return;
}
// H.264 maximum
int maxResolutionDimension = 4096;
if (@available(iOS 11.0, tvOS 11.0, *)) {
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC)) {
// HEVC maximum
maxResolutionDimension = 8192;
}
}
// Cap to maximum valid dimensions
width = MIN(width, maxResolutionDimension);
height = MIN(height, maxResolutionDimension);
// Cap to minimum valid dimensions
width = MAX(width, 256);
height = MAX(height, 256);
resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX] = CGSizeMake(width, height);
[self updateBitrate];
[self updateResolutionDisplayViewText];
self->_lastSelectedResolutionIndex = [self.resolutionSelector selectedSegmentIndex];
UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"Custom Resolution Selected" message: @"Custom resolutions are not officially supported by GeForce Experience, so it will not set your host display resolution. You will need to set it manually while in game.\n\nResolutions that are not supported by your client or host PC may cause streaming errors." preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
[self presentViewController:alertController animated:YES completion:nil];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:^(UIAlertAction *action) {
// Restore the previous selection
[self.resolutionSelector setSelectedSegmentIndex:self->_lastSelectedResolutionIndex];
}]];
[self presentViewController:alertController animated:YES completion:nil];
}
- (void)resolutionDisplayViewTapped:(UITapGestureRecognizer *)sender {
NSURL *url = [NSURL URLWithString:@"https://moonlight-stream.org/custom-resolution"];
if ([[UIApplication sharedApplication] canOpenURL:url]) {
[[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil];
}
}
- (void) updateResolutionDisplayViewText {
NSInteger width = [self getChosenStreamWidth];
NSInteger height = [self getChosenStreamHeight];
CGFloat viewFrameWidth = self.resolutionDisplayView.frame.size.width;
CGFloat viewFrameHeight = self.resolutionDisplayView.frame.size.height;
CGFloat padding = 10;
CGFloat fontSize = [UIFont smallSystemFontSize];
for (UIView *subview in self.resolutionDisplayView.subviews) {
[subview removeFromSuperview];
}
UILabel *label1 = [[UILabel alloc] init];
label1.text = @"Set PC/Game resolution: ";
label1.font = [UIFont systemFontOfSize:fontSize];
[label1 sizeToFit];
label1.frame = CGRectMake(padding, (viewFrameHeight - label1.frame.size.height) / 2, label1.frame.size.width, label1.frame.size.height);
UILabel *label2 = [[UILabel alloc] init];
label2.text = [NSString stringWithFormat:@"%ld x %ld", (long)width, (long)height];
[label2 sizeToFit];
label2.frame = CGRectMake(viewFrameWidth - label2.frame.size.width - padding, (viewFrameHeight - label2.frame.size.height) / 2, label2.frame.size.width, label2.frame.size.height);
[self.resolutionDisplayView addSubview:label1];
[self.resolutionDisplayView addSubview:label2];
}
- (void) bitrateSliderMoved {
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
@@ -282,14 +480,46 @@ static const int bitrateTable[] = {
}
}
- (uint32_t) getChosenCodecPreference {
// Auto is always the last segment
if (self.codecSelector.selectedSegmentIndex == self.codecSelector.numberOfSegments - 1) {
return CODEC_PREF_AUTO;
}
else {
switch (self.codecSelector.selectedSegmentIndex) {
case 0:
return CODEC_PREF_H264;
case 1:
return CODEC_PREF_HEVC;
case 2:
return CODEC_PREF_AV1;
default:
abort();
}
}
}
- (NSInteger) getChosenStreamHeight {
const int resolutionTable[] = { 360, 720, 1080, 2160 };
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]];
// because the 4k resolution can be removed
BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
if (lastSegmentSelected) {
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].height;
}
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].height;
}
- (NSInteger) getChosenStreamWidth {
// Assumes fixed 16:9 aspect ratio
return ([self getChosenStreamHeight] * 16) / 9;
// because the 4k resolution can be removed
BOOL lastSegmentSelected = [self.resolutionSelector selectedSegmentIndex] + 1 == [self.resolutionSelector numberOfSegments];
if (lastSegmentSelected) {
return resolutionTable[RESOLUTION_TABLE_CUSTOM_INDEX].width;
}
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]].width;
}
- (void) saveSettings {
@@ -300,21 +530,27 @@ static const int bitrateTable[] = {
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
BOOL swapABXYButtons = [self.swapABXYButtonsSelector selectedSegmentIndex] == 1;
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
BOOL useHevc = [self.hevcSelector selectedSegmentIndex] == 1;
uint32_t preferredCodec = [self getChosenCodecPreference];
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
BOOL enableHdr = [self.hdrSelector selectedSegmentIndex] == 1;
[dataMan saveSettingsWithBitrate:_bitrate
framerate:framerate
height:height
width:width
audioConfig:2 // Stereo
onscreenControls:onscreenControls
optimizeGames:optimizeGames
multiController:multiController
swapABXYButtons:swapABXYButtons
audioOnPC:audioOnPC
useHevc:useHevc
enableHdr:NO
preferredCodec:preferredCodec
useFramePacing:useFramePacing
enableHdr:enableHdr
btMouseSupport:btMouseSupport
absoluteTouchMode:absoluteTouchMode
statsOverlay:statsOverlay];
@@ -15,10 +15,12 @@
#if TARGET_OS_TV
@import GameController;
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, InputPresenceDelegate, UserInteractionDelegate, UIScrollViewDelegate>
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
#else
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, InputPresenceDelegate, UserInteractionDelegate, UIScrollViewDelegate>
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
#endif
@property (nonatomic) StreamConfiguration* streamConfig;
-(void)updatePreferredDisplayMode:(BOOL)streamActive;
@end
@@ -18,6 +18,18 @@
#include <arpa/inet.h>
#include <Limelight.h>
#if TARGET_OS_TV
#import <AVFoundation/AVDisplayCriteria.h>
#import <AVKit/AVDisplayManager.h>
#import <AVKit/UIWindow.h>
#endif
@interface AVDisplayCriteria()
@property(readonly) int videoDynamicRange;
@property(readonly, nonatomic) float refreshRate;
- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
@end
@implementation StreamFrameViewController {
ControllerSupport *_controllerSupport;
StreamManager *_streamMan;
@@ -92,7 +104,7 @@
[_spinner startAnimating];
_spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig presenceDelegate:self];
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig delegate:self];
_inactivityTimer = nil;
_streamView = [[StreamView alloc] initWithFrame:self.view.frame];
@@ -294,6 +306,9 @@
}
- (void) returnToMainFrame {
// Reset display mode back to default
[self updatePreferredDisplayMode:NO];
[_statsUpdateTimer invalidate];
_statsUpdateTimer = nil;
@@ -362,6 +377,8 @@
[self->_streamView showOnScreenControls];
[self->_controllerSupport connectionEstablished];
if (self->_settings.statsOverlay) {
self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
target:self
@@ -416,10 +433,27 @@
message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
break;
default:
title = @"Connection Terminated";
message = @"The connection was terminated";
case ML_ERROR_FRAME_CONVERSION:
title = @"Connection Error";
message = @"The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution.";
break;
default:
{
NSString* errorString;
if (abs(errorCode) > 1000) {
// We'll assume large errors are hex values
errorString = [NSString stringWithFormat:@"%08X", (uint32_t)errorCode];
}
else {
// Smaller values will just be printed as decimal (probably errno.h values)
errorString = [NSString stringWithFormat:@"%d", errorCode];
}
title = @"Connection Terminated";
message = [NSString stringWithFormat: @"The connection was terminated\n\nError code: %@", errorString];
break;
}
}
}
@@ -506,6 +540,24 @@
[_controllerSupport rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
}
- (void) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger {
Log(LOG_I, @"Trigger rumble on gamepad %d: %04x %04x", controllerNumber, leftTrigger, rightTrigger);
[_controllerSupport rumbleTriggers:controllerNumber leftTrigger:leftTrigger rightTrigger:rightTrigger];
}
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
Log(LOG_I, @"Set motion state on gamepad %d: %02x %u Hz", controllerNumber, motionType, reportRateHz);
[_controllerSupport setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
}
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
Log(LOG_I, @"Set controller LED on gamepad %d: l%02x%02x%02x", controllerNumber, r, g, b);
[_controllerSupport setControllerLed:controllerNumber r:r g:g b:b];
}
- (void)connectionStatusUpdate:(int)status {
Log(LOG_W, @"Connection status update: %d", status);
@@ -532,6 +584,42 @@
});
}
- (void) updatePreferredDisplayMode:(BOOL)streamActive {
#if TARGET_OS_TV
if (@available(tvOS 11.2, *)) {
UIWindow* window = [[[UIApplication sharedApplication] delegate] window];
AVDisplayManager* displayManager = [window avDisplayManager];
// This logic comes from Kodi and MrMC
if (streamActive) {
int dynamicRange;
if (LiGetCurrentHostDisplayHdrMode()) {
dynamicRange = 2; // HDR10
}
else {
dynamicRange = 0; // SDR
}
AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue]
videoDynamicRange:dynamicRange];
displayManager.preferredDisplayCriteria = displayCriteria;
}
else {
// Switch back to the default display mode
displayManager.preferredDisplayCriteria = nil;
}
}
#endif
}
- (void) setHdrMode:(bool)enabled {
Log(LOG_I, @"HDR is now: %s", enabled ? "active" : "inactive");
dispatch_async(dispatch_get_main_queue(), ^{
[self updatePreferredDisplayMode:YES];
});
}
- (void) videoContentShown {
[_spinner stopAnimating];
[self.view setBackgroundColor:[UIColor blackColor]];
@@ -559,6 +647,12 @@
#endif
}
- (void) streamExitRequested {
Log(LOG_I, @"Gamepad combo requested stream exit");
[self returnToMainFrame];
}
- (void)userInteractionBegan {
// Disable hiding home bar when user is interacting.
// iOS will force it to be shown anyway, but it will
+2
View File
@@ -54,6 +54,8 @@
<array>
<string>arm64</string>
</array>
<key>LSSupportsGameMode</key>
<true/>
<key>UIUserInterfaceStyle</key>
<string>Dark</string>
</dict>
+74 -4
View File
@@ -25,12 +25,14 @@
<array>
<string>0</string>
<string>1</string>
<string>3</string>
<string>2</string>
</array>
<key>Titles</key>
<array>
<string>720p</string>
<string>1080p</string>
<string>1440p</string>
<string>4K</string>
</array>
</dict>
@@ -73,6 +75,8 @@
<string>20 Mbps</string>
<string>25 Mbps</string>
<string>50 Mbps</string>
<string>70 Mbps</string>
<string>80 Mbps</string>
<string>100 Mbps</string>
<string>150 Mbps</string>
</array>
@@ -86,10 +90,34 @@
<string>20000</string>
<string>25000</string>
<string>50000</string>
<string>70000</string>
<string>80000</string>
<string>100000</string>
<string>150000</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Audio Configuration</string>
<key>Key</key>
<string>audioConfig</string>
<key>DefaultValue</key>
<string>2</string>
<key>Titles</key>
<array>
<string>Stereo</string>
<string>LPCM 5.1</string>
<string>LPCM 7.1</string>
</array>
<key>Values</key>
<array>
<string>2</string>
<string>6</string>
<string>8</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSGroupSpecifier</string>
@@ -132,6 +160,16 @@
<key>DefaultValue</key>
<true/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<key>Title</key>
<string>Swap A/B and X/Y Buttons</string>
<key>Key</key>
<string>swapABXYButtons</string>
<key>DefaultValue</key>
<false/>
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
@@ -150,13 +188,45 @@
</dict>
<dict>
<key>Type</key>
<string>PSToggleSwitchSpecifier</string>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Use HEVC Codec</string>
<string>Preferred Codec</string>
<key>Key</key>
<string>useHevc</string>
<string>preferredCodec</string>
<key>DefaultValue</key>
<true/>
<string>0</string>
<key>Values</key>
<array>
<string>0</string>
<string>1</string>
<string>2</string>
</array>
<key>Titles</key>
<array>
<string>Auto</string>
<string>H.264</string>
<string>HEVC</string>
</array>
</dict>
<dict>
<key>Type</key>
<string>PSMultiValueSpecifier</string>
<key>Title</key>
<string>Frame Pacing Preference</string>
<key>Key</key>
<string>useFramePacing</string>
<key>DefaultValue</key>
<string>0</string>
<key>Values</key>
<array>
<string>0</string>
<string>1</string>
</array>
<key>Titles</key>
<array>
<string>Lowest Latency</string>
<string>Smoothest Video</string>
</array>
</dict>
<dict>
<key>Type</key>
+4
View File
@@ -9,8 +9,12 @@
#import <UIKit/UIKit.h>
#import "AppDelegate.h"
#define SDL_MAIN_HANDLED
#import <SDL.h>
int main(int argc, char * argv[]) {
@autoreleasepool {
SDL_SetMainReady();
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
}
+163 -227
View File
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
objectVersion = 48;
objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -24,15 +24,26 @@
9865DC38213287FE0005B9B9 /* LoadingFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB4A23B71A9D3637004D2EF2 /* LoadingFrameViewController.m */; };
9865DC3C2132922E0005B9B9 /* GameController.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9865DC3B2132922E0005B9B9 /* GameController.framework */; };
9865DC3E21332D660005B9B9 /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A032AF60F5300C5A11C /* libavcodec.a */; };
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A052AF60F5300C5A11C /* libavformat.a */; };
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A042AF60F5300C5A11C /* libavutil.a */; };
98882A0C2AF60F9E00C5A11C /* libavcodec.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A0A2AF60F9000C5A11C /* libavcodec.a */; };
98882A0D2AF60FA000C5A11C /* libavformat.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A0B2AF60F9000C5A11C /* libavformat.a */; };
98882A0E2AF60FA300C5A11C /* libavutil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98882A092AF60F9000C5A11C /* libavutil.a */; };
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = 988FCD40293B091B003050E2 /* KeyboardInputField.m */; };
988FCD42293B091B003050E2 /* KeyboardInputField.m in Sources */ = {isa = PBXBuildFile; fileRef = 988FCD40293B091B003050E2 /* KeyboardInputField.m */; };
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 9890CF6A203B7EE1006C4B06 /* libxml2.tbd */; };
9896219723D15C7000211983 /* X1Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9896219623D15C7000211983 /* X1Kit.swift */; };
9896219A23D56E8100211983 /* X1Kit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9896219623D15C7000211983 /* X1Kit.swift */; };
9897B6A1221260EF00966419 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 9897B6A0221260EF00966419 /* Controller.m */; };
9897B6A62212732C00966419 /* Controller.m in Sources */ = {isa = PBXBuildFile; fileRef = 9897B6A0221260EF00966419 /* Controller.m */; };
98B9CE6D27B2144B00B473C4 /* AVKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 98B9CE6C27B2144B00B473C4 /* AVKit.framework */; };
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 98AB2E841CAD46840089BB98 /* libmoonlight-common.a */; };
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */; };
98D585701C0ED0E800F6CC00 /* TemporarySettings.m in Sources */ = {isa = PBXBuildFile; fileRef = 98D5856F1C0ED0E800F6CC00 /* TemporarySettings.m */; };
DC1F5A07206436B20037755F /* ConnectionHelper.m in Sources */ = {isa = PBXBuildFile; fileRef = DC1F5A06206436B20037755F /* ConnectionHelper.m */; };
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEF9B2C1A705600224667 /* OpenSSL */; };
F74BEFA42C1A80E400224667 /* OpenSSL in Frameworks */ = {isa = PBXBuildFile; productRef = F74BEFA32C1A80E400224667 /* OpenSSL */; };
FB1A674D2131E65900507771 /* KeyboardSupport.m in Sources */ = {isa = PBXBuildFile; fileRef = FB1A674C2131E65900507771 /* KeyboardSupport.m */; };
FB1A67602132419700507771 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FB1A675E2132419700507771 /* Main.storyboard */; };
FB1A67622132419A00507771 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = FB1A67612132419A00507771 /* Assets.xcassets */; };
@@ -69,8 +80,6 @@
FB1A67E02132460A00507771 /* Logger.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD1C8E11A8AD71400C6703C /* Logger.m */; };
FB1A67E32132498A00507771 /* Limelight.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */; };
FB1A67E521324A1F00507771 /* CoreData.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A67E421324A1F00507771 /* CoreData.framework */; };
FB1A67E621324DD600507771 /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E019F6AFB800339C8A /* libcrypto.a */; };
FB1A67E721324DD600507771 /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E119F6AFB800339C8A /* libssl.a */; };
FB1A67E821324DE300507771 /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; };
FB1A67EA21324DF300507771 /* libxml2.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = FB1A67E921324DF300507771 /* libxml2.tbd */; };
FB1A6819213284FB00507771 /* UIComputerView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86DF19F7A837001C18A8 /* UIComputerView.m */; };
@@ -113,8 +122,6 @@
FB89463519F646E200339C8A /* MainFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462519F646E200339C8A /* MainFrameViewController.m */; };
FB89463619F646E200339C8A /* StreamFrameViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = FB89462719F646E200339C8A /* StreamFrameViewController.m */; };
FB89463819F6473800339C8A /* Launch Screen.xib in Resources */ = {isa = PBXBuildFile; fileRef = FB89463719F6473800339C8A /* Launch Screen.xib */; };
FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E019F6AFB800339C8A /* libcrypto.a */; };
FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E119F6AFB800339C8A /* libssl.a */; };
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; };
FB9AFD281A7C84ED00872C98 /* HttpResponse.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD271A7C84ED00872C98 /* HttpResponse.m */; };
FB9AFD321A7D867C00872C98 /* AppAssetRetriever.m in Sources */ = {isa = PBXBuildFile; fileRef = FB9AFD311A7D867C00872C98 /* AppAssetRetriever.m */; };
@@ -163,6 +170,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
566E9D2B2770B23A00EF7BFE /* Moonlight v1.7.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.7.xcdatamodel"; sourceTree = "<group>"; };
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
9803CCAB254F9EAF00EE185E /* ConnectionCallbacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ConnectionCallbacks.h; sourceTree = "<group>"; };
98132E8C20BC9A62007A053F /* Moonlight v1.1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.1.xcdatamodel"; sourceTree = "<group>"; };
@@ -178,16 +186,28 @@
9827E7A7251436EA00F25707 /* HapticContext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = HapticContext.h; sourceTree = "<group>"; };
9832D1341BBCD5C50036EF48 /* TemporaryApp.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryApp.h; path = Database/TemporaryApp.h; sourceTree = "<group>"; };
9832D1351BBCD5C50036EF48 /* TemporaryApp.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryApp.m; path = Database/TemporaryApp.m; sourceTree = "<group>"; };
984CD023288A310D0097D2D4 /* Moonlight v1.8.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.8.xcdatamodel"; sourceTree = "<group>"; };
98517B1B21CE0A9000481377 /* Moonlight v1.3.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.3.xcdatamodel"; sourceTree = "<group>"; };
98608BDD22DC0C2C000E5672 /* Moonlight v1.4.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.4.xcdatamodel"; sourceTree = "<group>"; };
9865DC3B2132922E0005B9B9 /* GameController.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = GameController.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS11.4.sdk/System/Library/Frameworks/GameController.framework; sourceTree = DEVELOPER_DIR; };
986CCE6C2133E45300168291 /* Moonlight v1.2.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.2.xcdatamodel"; sourceTree = "<group>"; };
986E28A528EA989100758361 /* Moonlight v1.9.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.9.xcdatamodel"; sourceTree = "<group>"; };
9874E8962AE95E1D00130A3C /* Moonlight v1.10.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.10.xcdatamodel"; sourceTree = "<group>"; };
98783FEA242EAC5D00F00EF4 /* Moonlight v1.5.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "Moonlight v1.5.xcdatamodel"; sourceTree = "<group>"; };
98882A032AF60F5300C5A11C /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = libs/FFmpeg/lib/iOS/libavcodec.a; sourceTree = "<group>"; };
98882A042AF60F5300C5A11C /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = libs/FFmpeg/lib/iOS/libavutil.a; sourceTree = "<group>"; };
98882A052AF60F5300C5A11C /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = libs/FFmpeg/lib/iOS/libavformat.a; sourceTree = "<group>"; };
98882A092AF60F9000C5A11C /* libavutil.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavutil.a; path = libs/FFmpeg/lib/tvOS/libavutil.a; sourceTree = "<group>"; };
98882A0A2AF60F9000C5A11C /* libavcodec.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavcodec.a; path = libs/FFmpeg/lib/tvOS/libavcodec.a; sourceTree = "<group>"; };
98882A0B2AF60F9000C5A11C /* libavformat.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; name = libavformat.a; path = libs/FFmpeg/lib/tvOS/libavformat.a; sourceTree = "<group>"; };
988FCD3F293B091B003050E2 /* KeyboardInputField.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyboardInputField.h; sourceTree = "<group>"; };
988FCD40293B091B003050E2 /* KeyboardInputField.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KeyboardInputField.m; sourceTree = "<group>"; };
9890CF6A203B7EE1006C4B06 /* libxml2.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libxml2.tbd; path = usr/lib/libxml2.tbd; sourceTree = SDKROOT; };
9896219623D15C7000211983 /* X1Kit.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = X1Kit.swift; path = X1Kit/Sources/X1Kit/X1Kit.swift; sourceTree = "<group>"; };
9897B6A0221260EF00966419 /* Controller.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = Controller.m; sourceTree = "<group>"; };
9897B6A32212610800966419 /* Controller.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = Controller.h; sourceTree = "<group>"; };
98AB2E7F1CAD46830089BB98 /* moonlight-common.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "moonlight-common.xcodeproj"; path = "moonlight-common/moonlight-common.xcodeproj"; sourceTree = "<group>"; };
98B9CE6C27B2144B00B473C4 /* AVKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVKit.framework; path = Platforms/AppleTVOS.platform/Developer/SDKs/AppleTVOS15.2.sdk/System/Library/Frameworks/AVKit.framework; sourceTree = DEVELOPER_DIR; };
98D5856B1C0EA79600F6CC00 /* TemporaryHost.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporaryHost.h; path = Database/TemporaryHost.h; sourceTree = "<group>"; };
98D5856C1C0EA79600F6CC00 /* TemporaryHost.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TemporaryHost.m; path = Database/TemporaryHost.m; sourceTree = "<group>"; };
98D5856E1C0ED0E800F6CC00 /* TemporarySettings.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TemporarySettings.h; path = Database/TemporarySettings.h; sourceTree = "<group>"; };
@@ -268,83 +288,6 @@
FB89462619F646E200339C8A /* StreamFrameViewController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = StreamFrameViewController.h; sourceTree = "<group>"; };
FB89462719F646E200339C8A /* StreamFrameViewController.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = StreamFrameViewController.m; sourceTree = "<group>"; };
FB89463719F6473800339C8A /* Launch Screen.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = "Launch Screen.xib"; sourceTree = SOURCE_ROOT; };
FB89469419F6AFB800339C8A /* aes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = aes.h; sourceTree = "<group>"; };
FB89469519F6AFB800339C8A /* asn1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1.h; sourceTree = "<group>"; };
FB89469619F6AFB800339C8A /* asn1_mac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1_mac.h; sourceTree = "<group>"; };
FB89469719F6AFB800339C8A /* asn1t.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = asn1t.h; sourceTree = "<group>"; };
FB89469819F6AFB800339C8A /* bio.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bio.h; sourceTree = "<group>"; };
FB89469919F6AFB800339C8A /* blowfish.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = blowfish.h; sourceTree = "<group>"; };
FB89469A19F6AFB800339C8A /* bn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bn.h; sourceTree = "<group>"; };
FB89469B19F6AFB800339C8A /* buffer.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = buffer.h; sourceTree = "<group>"; };
FB89469C19F6AFB800339C8A /* camellia.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = camellia.h; sourceTree = "<group>"; };
FB89469D19F6AFB800339C8A /* cast.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cast.h; sourceTree = "<group>"; };
FB89469E19F6AFB800339C8A /* cmac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cmac.h; sourceTree = "<group>"; };
FB89469F19F6AFB800339C8A /* cms.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cms.h; sourceTree = "<group>"; };
FB8946A019F6AFB800339C8A /* comp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = comp.h; sourceTree = "<group>"; };
FB8946A119F6AFB800339C8A /* conf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf.h; sourceTree = "<group>"; };
FB8946A219F6AFB800339C8A /* conf_api.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = conf_api.h; sourceTree = "<group>"; };
FB8946A319F6AFB800339C8A /* crypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = crypto.h; sourceTree = "<group>"; };
FB8946A419F6AFB800339C8A /* des.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = des.h; sourceTree = "<group>"; };
FB8946A519F6AFB800339C8A /* des_old.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = des_old.h; sourceTree = "<group>"; };
FB8946A619F6AFB800339C8A /* dh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dh.h; sourceTree = "<group>"; };
FB8946A719F6AFB800339C8A /* dsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dsa.h; sourceTree = "<group>"; };
FB8946A819F6AFB800339C8A /* dso.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dso.h; sourceTree = "<group>"; };
FB8946A919F6AFB800339C8A /* dtls1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = dtls1.h; sourceTree = "<group>"; };
FB8946AA19F6AFB800339C8A /* e_os2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = e_os2.h; sourceTree = "<group>"; };
FB8946AB19F6AFB800339C8A /* ebcdic.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ebcdic.h; sourceTree = "<group>"; };
FB8946AC19F6AFB800339C8A /* ec.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ec.h; sourceTree = "<group>"; };
FB8946AD19F6AFB800339C8A /* ecdh.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ecdh.h; sourceTree = "<group>"; };
FB8946AE19F6AFB800339C8A /* ecdsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ecdsa.h; sourceTree = "<group>"; };
FB8946AF19F6AFB800339C8A /* engine.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = engine.h; sourceTree = "<group>"; };
FB8946B019F6AFB800339C8A /* err.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = err.h; sourceTree = "<group>"; };
FB8946B119F6AFB800339C8A /* evp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = evp.h; sourceTree = "<group>"; };
FB8946B219F6AFB800339C8A /* hmac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = hmac.h; sourceTree = "<group>"; };
FB8946B319F6AFB800339C8A /* idea.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = idea.h; sourceTree = "<group>"; };
FB8946B419F6AFB800339C8A /* krb5_asn.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = krb5_asn.h; sourceTree = "<group>"; };
FB8946B519F6AFB800339C8A /* kssl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = kssl.h; sourceTree = "<group>"; };
FB8946B619F6AFB800339C8A /* lhash.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = lhash.h; sourceTree = "<group>"; };
FB8946B719F6AFB800339C8A /* md4.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = md4.h; sourceTree = "<group>"; };
FB8946B819F6AFB800339C8A /* md5.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = md5.h; sourceTree = "<group>"; };
FB8946B919F6AFB800339C8A /* mdc2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = mdc2.h; sourceTree = "<group>"; };
FB8946BA19F6AFB800339C8A /* modes.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = modes.h; sourceTree = "<group>"; };
FB8946BB19F6AFB800339C8A /* obj_mac.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = obj_mac.h; sourceTree = "<group>"; };
FB8946BC19F6AFB800339C8A /* objects.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = objects.h; sourceTree = "<group>"; };
FB8946BD19F6AFB800339C8A /* ocsp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ocsp.h; sourceTree = "<group>"; };
FB8946BE19F6AFB800339C8A /* opensslconf.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslconf.h; sourceTree = "<group>"; };
FB8946BF19F6AFB800339C8A /* opensslv.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opensslv.h; sourceTree = "<group>"; };
FB8946C019F6AFB800339C8A /* ossl_typ.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ossl_typ.h; sourceTree = "<group>"; };
FB8946C119F6AFB800339C8A /* pem.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pem.h; sourceTree = "<group>"; };
FB8946C219F6AFB800339C8A /* pem2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pem2.h; sourceTree = "<group>"; };
FB8946C319F6AFB800339C8A /* pkcs12.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pkcs12.h; sourceTree = "<group>"; };
FB8946C419F6AFB800339C8A /* pkcs7.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pkcs7.h; sourceTree = "<group>"; };
FB8946C519F6AFB800339C8A /* pqueue.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = pqueue.h; sourceTree = "<group>"; };
FB8946C619F6AFB800339C8A /* rand.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rand.h; sourceTree = "<group>"; };
FB8946C719F6AFB800339C8A /* rc2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rc2.h; sourceTree = "<group>"; };
FB8946C819F6AFB800339C8A /* rc4.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rc4.h; sourceTree = "<group>"; };
FB8946C919F6AFB800339C8A /* ripemd.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ripemd.h; sourceTree = "<group>"; };
FB8946CA19F6AFB800339C8A /* rsa.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = rsa.h; sourceTree = "<group>"; };
FB8946CB19F6AFB800339C8A /* safestack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = safestack.h; sourceTree = "<group>"; };
FB8946CC19F6AFB800339C8A /* seed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = seed.h; sourceTree = "<group>"; };
FB8946CD19F6AFB800339C8A /* sha.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = sha.h; sourceTree = "<group>"; };
FB8946CE19F6AFB800339C8A /* srp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = srp.h; sourceTree = "<group>"; };
FB8946CF19F6AFB800339C8A /* srtp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = srtp.h; sourceTree = "<group>"; };
FB8946D019F6AFB800339C8A /* ssl.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl.h; sourceTree = "<group>"; };
FB8946D119F6AFB800339C8A /* ssl2.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl2.h; sourceTree = "<group>"; };
FB8946D219F6AFB800339C8A /* ssl23.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl23.h; sourceTree = "<group>"; };
FB8946D319F6AFB800339C8A /* ssl3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ssl3.h; sourceTree = "<group>"; };
FB8946D419F6AFB800339C8A /* stack.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = stack.h; sourceTree = "<group>"; };
FB8946D519F6AFB800339C8A /* symhacks.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = symhacks.h; sourceTree = "<group>"; };
FB8946D619F6AFB800339C8A /* tls1.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = tls1.h; sourceTree = "<group>"; };
FB8946D719F6AFB800339C8A /* ts.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ts.h; sourceTree = "<group>"; };
FB8946D819F6AFB800339C8A /* txt_db.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = txt_db.h; sourceTree = "<group>"; };
FB8946D919F6AFB800339C8A /* ui.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ui.h; sourceTree = "<group>"; };
FB8946DA19F6AFB800339C8A /* ui_compat.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ui_compat.h; sourceTree = "<group>"; };
FB8946DB19F6AFB800339C8A /* whrlpool.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = whrlpool.h; sourceTree = "<group>"; };
FB8946DC19F6AFB800339C8A /* x509.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509.h; sourceTree = "<group>"; };
FB8946DD19F6AFB800339C8A /* x509_vfy.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509_vfy.h; sourceTree = "<group>"; };
FB8946DE19F6AFB800339C8A /* x509v3.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = x509v3.h; sourceTree = "<group>"; };
FB8946E019F6AFB800339C8A /* libcrypto.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libcrypto.a; sourceTree = "<group>"; };
FB8946E119F6AFB800339C8A /* libssl.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libssl.a; sourceTree = "<group>"; };
FB8946E519F6AFB800339C8A /* opus.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus.h; sourceTree = "<group>"; };
FB8946E619F6AFB800339C8A /* opus_defines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_defines.h; sourceTree = "<group>"; };
FB8946E719F6AFB800339C8A /* opus_multistream.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_multistream.h; sourceTree = "<group>"; };
@@ -386,12 +329,15 @@
files = (
9865DC30213260B40005B9B9 /* libmoonlight-common-tv.a in Frameworks */,
9865DC3C2132922E0005B9B9 /* GameController.framework in Frameworks */,
98882A0D2AF60FA000C5A11C /* libavformat.a in Frameworks */,
98882A0E2AF60FA300C5A11C /* libavutil.a in Frameworks */,
F74BEFA42C1A80E400224667 /* OpenSSL in Frameworks */,
FB1A67EA21324DF300507771 /* libxml2.tbd in Frameworks */,
FB1A67E821324DE300507771 /* libopus.a in Frameworks */,
98181BEB2791278F00E43572 /* libSDL2.a in Frameworks */,
FB1A67E621324DD600507771 /* libcrypto.a in Frameworks */,
FB1A67E721324DD600507771 /* libssl.a in Frameworks */,
FB1A67E521324A1F00507771 /* CoreData.framework in Frameworks */,
98B9CE6D27B2144B00B473C4 /* AVKit.framework in Frameworks */,
98882A0C2AF60F9E00C5A11C /* libavcodec.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -402,15 +348,17 @@
9890CF6B203B7EE1006C4B06 /* libxml2.tbd in Frameworks */,
98CFB82F1CAD481B0048EF74 /* libmoonlight-common.a in Frameworks */,
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */,
FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */,
FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */,
98882A082AF60F7400C5A11C /* libavutil.a in Frameworks */,
FB290CF419B2C406004C83CF /* CoreGraphics.framework in Frameworks */,
FB290CF819B2C406004C83CF /* CoreData.framework in Frameworks */,
98181BED2791281100E43572 /* CoreMotion.framework in Frameworks */,
F74BEF9C2C1A705600224667 /* OpenSSL in Frameworks */,
98882A072AF60F7200C5A11C /* libavformat.a in Frameworks */,
FB290CF619B2C406004C83CF /* UIKit.framework in Frameworks */,
FB290CF219B2C406004C83CF /* Foundation.framework in Frameworks */,
FB4679011A57048000377732 /* CoreFoundation.framework in Frameworks */,
98181BE92791277400E43572 /* libSDL2.a in Frameworks */,
98882A062AF60F7000C5A11C /* libavcodec.a in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -470,6 +418,13 @@
FB290CF019B2C406004C83CF /* Frameworks */ = {
isa = PBXGroup;
children = (
98882A032AF60F5300C5A11C /* libavcodec.a */,
98882A052AF60F5300C5A11C /* libavformat.a */,
98882A0A2AF60F9000C5A11C /* libavcodec.a */,
98882A0B2AF60F9000C5A11C /* libavformat.a */,
98882A042AF60F5300C5A11C /* libavutil.a */,
98882A092AF60F9000C5A11C /* libavutil.a */,
98B9CE6C27B2144B00B473C4 /* AVKit.framework */,
98181BEC2791281100E43572 /* CoreMotion.framework */,
98181BE82791275D00E43572 /* libSDL2.a */,
98181BEA2791278300E43572 /* libSDL2.a */,
@@ -562,6 +517,8 @@
9819CC13254F107A008A7C8E /* RelativeTouchHandler.m */,
9819CC20254F180F008A7C8E /* AbsoluteTouchHandler.h */,
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
);
path = Input;
sourceTree = "<group>";
@@ -629,120 +586,11 @@
FB89468F19F6AFB800339C8A /* libs */ = {
isa = PBXGroup;
children = (
FB89469119F6AFB800339C8A /* openssl */,
FB8946E219F6AFB800339C8A /* opus */,
);
path = libs;
sourceTree = "<group>";
};
FB89469119F6AFB800339C8A /* openssl */ = {
isa = PBXGroup;
children = (
FB89469219F6AFB800339C8A /* include */,
FB8946DF19F6AFB800339C8A /* lib */,
);
path = openssl;
sourceTree = "<group>";
};
FB89469219F6AFB800339C8A /* include */ = {
isa = PBXGroup;
children = (
FB89469319F6AFB800339C8A /* openssl */,
);
path = include;
sourceTree = "<group>";
};
FB89469319F6AFB800339C8A /* openssl */ = {
isa = PBXGroup;
children = (
FB89469419F6AFB800339C8A /* aes.h */,
FB89469519F6AFB800339C8A /* asn1.h */,
FB89469619F6AFB800339C8A /* asn1_mac.h */,
FB89469719F6AFB800339C8A /* asn1t.h */,
FB89469819F6AFB800339C8A /* bio.h */,
FB89469919F6AFB800339C8A /* blowfish.h */,
FB89469A19F6AFB800339C8A /* bn.h */,
FB89469B19F6AFB800339C8A /* buffer.h */,
FB89469C19F6AFB800339C8A /* camellia.h */,
FB89469D19F6AFB800339C8A /* cast.h */,
FB89469E19F6AFB800339C8A /* cmac.h */,
FB89469F19F6AFB800339C8A /* cms.h */,
FB8946A019F6AFB800339C8A /* comp.h */,
FB8946A119F6AFB800339C8A /* conf.h */,
FB8946A219F6AFB800339C8A /* conf_api.h */,
FB8946A319F6AFB800339C8A /* crypto.h */,
FB8946A419F6AFB800339C8A /* des.h */,
FB8946A519F6AFB800339C8A /* des_old.h */,
FB8946A619F6AFB800339C8A /* dh.h */,
FB8946A719F6AFB800339C8A /* dsa.h */,
FB8946A819F6AFB800339C8A /* dso.h */,
FB8946A919F6AFB800339C8A /* dtls1.h */,
FB8946AA19F6AFB800339C8A /* e_os2.h */,
FB8946AB19F6AFB800339C8A /* ebcdic.h */,
FB8946AC19F6AFB800339C8A /* ec.h */,
FB8946AD19F6AFB800339C8A /* ecdh.h */,
FB8946AE19F6AFB800339C8A /* ecdsa.h */,
FB8946AF19F6AFB800339C8A /* engine.h */,
FB8946B019F6AFB800339C8A /* err.h */,
FB8946B119F6AFB800339C8A /* evp.h */,
FB8946B219F6AFB800339C8A /* hmac.h */,
FB8946B319F6AFB800339C8A /* idea.h */,
FB8946B419F6AFB800339C8A /* krb5_asn.h */,
FB8946B519F6AFB800339C8A /* kssl.h */,
FB8946B619F6AFB800339C8A /* lhash.h */,
FB8946B719F6AFB800339C8A /* md4.h */,
FB8946B819F6AFB800339C8A /* md5.h */,
FB8946B919F6AFB800339C8A /* mdc2.h */,
FB8946BA19F6AFB800339C8A /* modes.h */,
FB8946BB19F6AFB800339C8A /* obj_mac.h */,
FB8946BC19F6AFB800339C8A /* objects.h */,
FB8946BD19F6AFB800339C8A /* ocsp.h */,
FB8946BE19F6AFB800339C8A /* opensslconf.h */,
FB8946BF19F6AFB800339C8A /* opensslv.h */,
FB8946C019F6AFB800339C8A /* ossl_typ.h */,
FB8946C119F6AFB800339C8A /* pem.h */,
FB8946C219F6AFB800339C8A /* pem2.h */,
FB8946C319F6AFB800339C8A /* pkcs12.h */,
FB8946C419F6AFB800339C8A /* pkcs7.h */,
FB8946C519F6AFB800339C8A /* pqueue.h */,
FB8946C619F6AFB800339C8A /* rand.h */,
FB8946C719F6AFB800339C8A /* rc2.h */,
FB8946C819F6AFB800339C8A /* rc4.h */,
FB8946C919F6AFB800339C8A /* ripemd.h */,
FB8946CA19F6AFB800339C8A /* rsa.h */,
FB8946CB19F6AFB800339C8A /* safestack.h */,
FB8946CC19F6AFB800339C8A /* seed.h */,
FB8946CD19F6AFB800339C8A /* sha.h */,
FB8946CE19F6AFB800339C8A /* srp.h */,
FB8946CF19F6AFB800339C8A /* srtp.h */,
FB8946D019F6AFB800339C8A /* ssl.h */,
FB8946D119F6AFB800339C8A /* ssl2.h */,
FB8946D219F6AFB800339C8A /* ssl23.h */,
FB8946D319F6AFB800339C8A /* ssl3.h */,
FB8946D419F6AFB800339C8A /* stack.h */,
FB8946D519F6AFB800339C8A /* symhacks.h */,
FB8946D619F6AFB800339C8A /* tls1.h */,
FB8946D719F6AFB800339C8A /* ts.h */,
FB8946D819F6AFB800339C8A /* txt_db.h */,
FB8946D919F6AFB800339C8A /* ui.h */,
FB8946DA19F6AFB800339C8A /* ui_compat.h */,
FB8946DB19F6AFB800339C8A /* whrlpool.h */,
FB8946DC19F6AFB800339C8A /* x509.h */,
FB8946DD19F6AFB800339C8A /* x509_vfy.h */,
FB8946DE19F6AFB800339C8A /* x509v3.h */,
);
path = openssl;
sourceTree = "<group>";
};
FB8946DF19F6AFB800339C8A /* lib */ = {
isa = PBXGroup;
children = (
FB8946E019F6AFB800339C8A /* libcrypto.a */,
FB8946E119F6AFB800339C8A /* libssl.a */,
);
path = lib;
sourceTree = "<group>";
};
FB8946E219F6AFB800339C8A /* opus */ = {
isa = PBXGroup;
children = (
@@ -854,6 +702,9 @@
FB1A68172132509800507771 /* PBXTargetDependency */,
);
name = "Moonlight TV";
packageProductDependencies = (
F74BEFA32C1A80E400224667 /* OpenSSL */,
);
productName = "Moonlight TV";
productReference = FB1A67532132419700507771 /* Moonlight TV.app */;
productType = "com.apple.product-type.application";
@@ -872,6 +723,9 @@
98AB2E861CAD468B0089BB98 /* PBXTargetDependency */,
);
name = Moonlight;
packageProductDependencies = (
F74BEF9B2C1A705600224667 /* OpenSSL */,
);
productName = Limelight;
productReference = FB290CEE19B2C406004C83CF /* Moonlight.app */;
productType = "com.apple.product-type.application";
@@ -882,8 +736,9 @@
FB290CE619B2C406004C83CF /* Project object */ = {
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 0730;
LastUpgradeCheck = 1200;
LastUpgradeCheck = 2700;
ORGANIZATIONNAME = "Moonlight Game Streaming Project";
TargetAttributes = {
FB1A67522132419700507771 = {
@@ -897,7 +752,6 @@
};
};
FB290CED19B2C406004C83CF = {
DevelopmentTeam = DM46QST4M7;
LastSwiftMigration = 1140;
};
};
@@ -911,6 +765,9 @@
Base,
);
mainGroup = FB290CE519B2C406004C83CF;
packageReferences = (
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */,
);
productRefGroup = FB290CEF19B2C406004C83CF /* Products */;
projectDirPath = "";
projectReferences = (
@@ -1015,6 +872,7 @@
9819CC1D254F1730008A7C8E /* RelativeTouchHandler.m in Sources */,
FB1A67AB213245C500507771 /* CryptoManager.m in Sources */,
FB1A67AF213245C500507771 /* IdManager.m in Sources */,
988FCD42293B091B003050E2 /* KeyboardInputField.m in Sources */,
FB1A67A3213245BD00507771 /* Connection.m in Sources */,
FB1A67A5213245BD00507771 /* StreamConfiguration.m in Sources */,
FB1A67A7213245BD00507771 /* StreamManager.m in Sources */,
@@ -1075,6 +933,7 @@
FB4A23B81A9D3637004D2EF2 /* LoadingFrameViewController.m in Sources */,
FB9AFD3A1A7E05CE00872C98 /* ServerInfoResponse.m in Sources */,
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
988FCD41293B091B003050E2 /* KeyboardInputField.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -1124,9 +983,8 @@
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = DM46QST4M7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
@@ -1137,14 +995,25 @@
"$(PROJECT_DIR)/libs/**",
);
INFOPLIST_FILE = "Moonlight TV/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=appletvos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/openssl/lib/tvOS",
"$(PROJECT_DIR)/libs/opus/lib/tvOS",
"$(PROJECT_DIR)/libs/SDL2/lib/tvOS",
"$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS",
);
MARKETING_VERSION = 7.1.3;
"LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/tvOS-Sim",
"$(PROJECT_DIR)/libs/SDL2/lib/tvOS-Sim",
"$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS-Sim",
);
MARKETING_VERSION = 9.0.2;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1154,7 +1023,7 @@
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 12.0;
TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)";
};
name = Debug;
};
@@ -1170,9 +1039,8 @@
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 2;
CURRENT_PROJECT_VERSION = 1;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = DM46QST4M7;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_INCREASE_PRECOMPILED_HEADER_SHARING = NO;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
@@ -1183,14 +1051,25 @@
"$(PROJECT_DIR)/libs/**",
);
INFOPLIST_FILE = "Moonlight TV/Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=appletvos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/openssl/lib/tvOS",
"$(PROJECT_DIR)/libs/opus/lib/tvOS",
"$(PROJECT_DIR)/libs/SDL2/lib/tvOS",
"$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS",
);
MARKETING_VERSION = 7.1.3;
"LIBRARY_SEARCH_PATHS[sdk=appletvsimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/tvOS-Sim",
"$(PROJECT_DIR)/libs/SDL2/lib/tvOS-Sim",
"$(PROJECT_DIR)/libs/FFmpeg/lib/tvOS-Sim",
);
MARKETING_VERSION = 9.0.2;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight";
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -1200,7 +1079,7 @@
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Moonlight-Swift.h";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 3;
TVOS_DEPLOYMENT_TARGET = 12.0;
TVOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_TVOS_DEPLOYMENT_TARGET)";
};
name = Release;
};
@@ -1208,6 +1087,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
@@ -1233,8 +1113,10 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
DEVELOPMENT_TEAM = DM46QST4M7;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
@@ -1250,9 +1132,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_OBJC_INTERFACE_HEADER_NAME = "";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1263,6 +1146,7 @@
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
@@ -1288,8 +1172,10 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = YES;
DEVELOPMENT_TEAM = DM46QST4M7;
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu99;
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -1302,9 +1188,10 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 9.3;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
PROVISIONING_PROFILE = "";
SDKROOT = iphoneos;
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_OBJC_INTERFACE_HEADER_NAME = "";
SWIFT_VERSION = 4.0;
TARGETED_DEVICE_FAMILY = "1,2";
@@ -1320,7 +1207,6 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
HEADER_SEARCH_PATHS = (
@@ -1331,15 +1217,26 @@
"$(PROJECT_DIR)/libs/**",
);
INFOPLIST_FILE = "Limelight/Limelight-Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/iOS",
"$(PROJECT_DIR)/libs/openssl/lib/iOS",
"$(PROJECT_DIR)/libs/SDL2/lib/iOS",
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS",
);
MARKETING_VERSION = 7.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.$(PRODUCT_NAME:rfc1034identifier)";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/iOS-Sim",
"$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim",
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
);
MARKETING_VERSION = 9.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
SKIP_INSTALL = NO;
@@ -1360,7 +1257,6 @@
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 1;
ENABLE_BITCODE = YES;
GCC_PRECOMPILE_PREFIX_HEADER = YES;
GCC_PREFIX_HEADER = "Limelight/Limelight-Prefix.pch";
HEADER_SEARCH_PATHS = (
@@ -1371,21 +1267,33 @@
"$(PROJECT_DIR)/libs/**",
);
INFOPLIST_FILE = "Limelight/Limelight-Info.plist";
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
LIBRARY_SEARCH_PATHS = (
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.games";
IPHONEOS_DEPLOYMENT_TARGET = "$(RECOMMENDED_IPHONEOS_DEPLOYMENT_TARGET)";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/iOS",
"$(PROJECT_DIR)/libs/openssl/lib/iOS",
"$(PROJECT_DIR)/libs/SDL2/lib/iOS",
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS",
);
MARKETING_VERSION = 7.2.0;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.$(PRODUCT_NAME:rfc1034identifier)";
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*]" = (
"$(inherited)",
"$(PROJECT_DIR)/libs/opus/lib/iOS-Sim",
"$(PROJECT_DIR)/libs/SDL2/lib/iOS-Sim",
"$(PROJECT_DIR)/libs/FFmpeg/lib/iOS-Sim",
);
MARKETING_VERSION = 9.0.2;
PRODUCT_BUNDLE_IDENTIFIER = "com.moonlight-stream.Moonlight";
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE = "";
SKIP_INSTALL = NO;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_OBJC_BRIDGING_HEADER = "Limelight/Input/Moonlight-Bridging-Header.h";
SWIFT_OBJC_INTERFACE_HEADER_NAME = "Moonlight-Swift.h";
SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule";
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_SWIFT3_OBJC_INFERENCE = Default;
SWIFT_VERSION = 5.0;
WRAPPER_EXTENSION = app;
@@ -1424,10 +1332,38 @@
};
/* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */
F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */ = {
isa = XCRemoteSwiftPackageReference;
repositoryURL = "https://github.com/krzyzanowskim/OpenSSL-Package.git";
requirement = {
kind = upToNextMajorVersion;
minimumVersion = 3.1.5004;
};
};
/* End XCRemoteSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
F74BEF9B2C1A705600224667 /* OpenSSL */ = {
isa = XCSwiftPackageProductDependency;
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
productName = OpenSSL;
};
F74BEFA32C1A80E400224667 /* OpenSSL */ = {
isa = XCSwiftPackageProductDependency;
package = F74BEF9A2C1A705600224667 /* XCRemoteSwiftPackageReference "OpenSSL-Package" */;
productName = OpenSSL;
};
/* End XCSwiftPackageProductDependency section */
/* Begin XCVersionGroup section */
FB290D0519B2C406004C83CF /* Limelight.xcdatamodeld */ = {
isa = XCVersionGroup;
children = (
9874E8962AE95E1D00130A3C /* Moonlight v1.10.xcdatamodel */,
986E28A528EA989100758361 /* Moonlight v1.9.xcdatamodel */,
984CD023288A310D0097D2D4 /* Moonlight v1.8.xcdatamodel */,
566E9D2B2770B23A00EF7BFE /* Moonlight v1.7.xcdatamodel */,
9819CC25254F2734008A7C8E /* Moonlight v1.6.xcdatamodel */,
98783FEA242EAC5D00F00EF4 /* Moonlight v1.5.xcdatamodel */,
98608BDD22DC0C2C000E5672 /* Moonlight v1.4.xcdatamodel */,
@@ -1440,7 +1376,7 @@
FB4678F21A51BDCB00377732 /* Limelight 0.3.0.xcdatamodel */,
FB290D0619B2C406004C83CF /* Limelight.xcdatamodel */,
);
currentVersion = 9819CC25254F2734008A7C8E /* Moonlight v1.6.xcdatamodel */;
currentVersion = 9874E8962AE95E1D00130A3C /* Moonlight v1.10.xcdatamodel */;
path = Limelight.xcdatamodeld;
sourceTree = "<group>";
versionGroupType = wrapper.xcdatamodel;
@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "self:Moonlight.xcodeproj">
location = "self:">
</FileRef>
</Workspace>
@@ -0,0 +1,15 @@
{
"originHash" : "f17bf6f1131c7255a75ffd6a989b69573cb49c332e612dffb429905d8d7afe72",
"pins" : [
{
"identity" : "openssl-package",
"kind" : "remoteSourceControl",
"location" : "https://github.com/krzyzanowskim/OpenSSL-Package.git",
"state" : {
"revision" : "a3809db22a45e3ff658377a24da252982a306b9b",
"version" : "3.6.3000"
}
}
],
"version" : 3
}
+3 -4
View File
@@ -1,9 +1,8 @@
# Moonlight iOS/tvOS
[![AppVeyor Build Status](https://ci.appveyor.com/api/projects/status/kwv8vpwr457lqn25/branch/master?svg=true)](https://ci.appveyor.com/project/cgutman/moonlight-ios/branch/master)
[![CI](https://github.com/moonlight-stream/moonlight-ios/actions/workflows/ci.yml/badge.svg)](https://github.com/moonlight-stream/moonlight-ios/actions/workflows/ci.yml)
[Moonlight for iOS/tvOS](https://moonlight-stream.org) is an open source implementation of NVIDIA's GameStream, as used by the NVIDIA Shield, but built for iOS and tvOS. Moonlight for iOS/tvOS allows you to stream your full collection of Steam games from
your powerful desktop computer to your iOS device or Apple TV.
[Moonlight for iOS/tvOS](https://moonlight-stream.org) is an open source client for [Sunshine](https://github.com/LizardByte/Sunshine) and NVIDIA GameStream. Moonlight for iOS/tvOS allows you to stream your full collection of games and apps from your powerful desktop computer to your iOS device or Apple TV.
Moonlight also has a [PC client](https://github.com/moonlight-stream/moonlight-qt) and [Android client](https://github.com/moonlight-stream/moonlight-android).
@@ -12,7 +11,7 @@ Check out [the Moonlight wiki](https://github.com/moonlight-stream/moonlight-doc
[![Moonlight for iOS and tvOS](https://moonlight-stream.org/images/App_Store_Badge_135x40.svg)](https://apps.apple.com/us/app/moonlight-game-streaming/id1000551566)
## Building
* Install Xcode 13 from the [App Store page](https://apps.apple.com/us/app/xcode/id497799835)
* Install Xcode from the [App Store page](https://apps.apple.com/us/app/xcode/id497799835)
* Run `git clone --recursive https://github.com/moonlight-stream/moonlight-ios.git`
* If you've already clone the repo without `--recursive`, run `git submodule update --init --recursive`
* Open Moonlight.xcodeproj in Xcode
-15
View File
@@ -1,15 +0,0 @@
version: 0.0.0.{build}
clone_depth: 1
image: macOS
before_build:
- 'git submodule update --init --recursive'
build_script:
- sh: set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "Moonlight" -destination "platform=iOS Simulator,OS=14.3,name=iPhone 11 Pro" clean build | xcpretty
- sh: set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "Moonlight" -destination "platform=iOS Simulator,OS=14.3,name=iPad Pro (9.7-inch)" clean build | xcpretty
- sh: set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "Moonlight TV" -destination "platform=tvOS Simulator,OS=14.3,name=Apple TV 4K" clean build | xcpretty
deploy: off
+108 -39
View File
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
<device id="ipad11_0rounded" orientation="landscape" layout="fullscreen" appearance="dark"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="EVd-wq-ego">
<device id="ipad11_0rounded" orientation="portrait" layout="fullscreen" appearance="dark"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -12,7 +12,7 @@
<objects>
<viewController id="wb7-af-jn8" customClass="MainFrameViewController" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" indicatorStyle="black" dataMode="prototypes" id="TZj-Lc-M9d" customClass="AppCollectionView">
<rect key="frame" x="0.0" y="0.0" width="1194" height="784"/>
<rect key="frame" x="0.0" y="0.0" width="834" height="1120"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="20" minimumInteritemSpacing="20" id="f7l-kG-hJc">
@@ -58,8 +58,8 @@
<viewControllerLayoutGuide type="top" id="ef6-uF-5Hh"/>
<viewControllerLayoutGuide type="bottom" id="XvO-F3-j4u"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="WRy-3f-gEP">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<view key="view" contentMode="scaleToFill" id="WRy-3f-gEP" customClass="UIScrollView">
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Resolution" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="pYZ-GR-EwO">
@@ -70,20 +70,28 @@
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="ckc-Dm-8ex">
<rect key="frame" x="16" y="49" width="209" height="29"/>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="ckc-Dm-8ex">
<rect key="frame" x="16" y="47" width="459" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="360p"/>
<segment title="720p"/>
<segment title="1080p"/>
<segment title="4K"/>
<segment title="Safe Area"/>
<segment title="Full"/>
<segment title="Custom"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="9cG-JD-glb" userLabel="Resolution Display View">
<rect key="frame" x="22" y="89" width="447" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Frame Rate" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Aiv-r8-9k2">
<rect key="frame" x="16" y="85" width="88" height="21"/>
<rect key="frame" x="16" y="132" width="88" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@@ -91,7 +99,7 @@
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="lGK-vl-pdw">
<rect key="frame" x="16" y="114" width="209" height="29"/>
<rect key="frame" x="16" y="161" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="30 FPS"/>
@@ -102,7 +110,7 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Bitrate: 10 Mbps" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lMt-4H-fkV">
<rect key="frame" x="16" y="150" width="151" height="21"/>
<rect key="frame" x="16" y="197" width="151" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@@ -110,19 +118,19 @@
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="20" minValue="1" maxValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="JAY-nj-UNz">
<rect key="frame" x="14" y="179" width="213" height="31"/>
<rect key="frame" x="14" y="226" width="459" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Touch Mode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="h5g-U1-Z2J" userLabel="Touch Mode">
<rect key="frame" x="16" y="208" width="186" height="21"/>
<rect key="frame" x="16" y="255" width="186" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="e4G-id-vjI" userLabel="Touch Mode Selector">
<rect key="frame" x="16" y="237" width="209" height="32"/>
<rect key="frame" x="16" y="284" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Touchpad"/>
@@ -132,7 +140,7 @@
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="On-Screen Controls" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="LGV-5y-piG">
<rect key="frame" x="16" y="276" width="153" height="21"/>
<rect key="frame" x="16" y="323" width="153" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" red="0.81533776120000001" green="0.85979419950000002" blue="0.91372549020000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
@@ -140,7 +148,7 @@
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="qSU-wh-tqA">
<rect key="frame" x="16" y="305" width="209" height="29"/>
<rect key="frame" x="16" y="352" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Off"/>
@@ -152,14 +160,14 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Optimize Game Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WSI-9V-G6c">
<rect key="frame" x="16" y="341" width="186" height="21"/>
<rect key="frame" x="16" y="388" width="186" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="nCO-OT-dV1" userLabel="Optimize Settings Selector">
<rect key="frame" x="16" y="370" width="209" height="28"/>
<rect key="frame" x="16" y="417" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -169,14 +177,14 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Multi-Controller Mode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bFD-Ng-abn">
<rect key="frame" x="16" y="406" width="169" height="21"/>
<rect key="frame" x="16" y="453" width="169" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="KlC-fG-wEi" userLabel="MC Selector">
<rect key="frame" x="16" y="435" width="209" height="28"/>
<rect key="frame" x="16" y="482" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Single"/>
@@ -185,15 +193,22 @@
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Swap A/B and X/Y Buttons" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Dll-7x-18l">
<rect key="frame" x="16" y="517" width="217" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Play Audio on PC" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="fXq-oD-MrL">
<rect key="frame" x="16" y="471" width="131" height="21"/>
<rect key="frame" x="16" y="583" width="131" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="WDY-9o-6Hb" userLabel="PC Audio Selector">
<rect key="frame" x="16" y="500" width="209" height="28"/>
<rect key="frame" x="16" y="612" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -202,32 +217,68 @@
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HEVC Video" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6w5-Md-NpV">
<rect key="frame" x="16" y="536" width="93" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Preferred Codec" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6w5-Md-NpV" userLabel="Preferred Codec">
<rect key="frame" x="16" y="648" width="126" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="aFy-0w-YPe" userLabel="HEVC Selector">
<rect key="frame" x="16" y="565" width="209" height="28"/>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="3" translatesAutoresizingMaskIntoConstraints="NO" id="aFy-0w-YPe" userLabel="Codec Selector">
<rect key="frame" x="16" y="675" width="459" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="H.264"/>
<segment title="HEVC"/>
<segment title="AV1"/>
<segment title="Auto"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HDR (Beta)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="bZL-t1-mk7">
<rect key="frame" x="16" y="712" width="86" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="OA2-SA-bBO" userLabel="HDR Selector">
<rect key="frame" x="16" y="741" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
<segment title="Yes"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Frame Pacing Preference" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ZWC-Gw-pSq">
<rect key="frame" x="16" y="776" width="459" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="7va-uJ-IfD" userLabel="framePacingSelector">
<rect key="frame" x="16" y="805" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Lowest Latency"/>
<segment title="Smoothest Video"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Citrix X1 Mouse Support" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="t9f-V8-GCm">
<rect key="frame" x="16" y="600" width="186" height="21"/>
<rect key="frame" x="16" y="840" width="186" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="KMg-3j-F9p" userLabel="Citrix X1 Selector">
<rect key="frame" x="16" y="629" width="209" height="28"/>
<rect key="frame" x="16" y="869" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -237,14 +288,24 @@
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Statistics Overlay" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="s2i-ZL-Gz4" userLabel="Statistics Overlay">
<rect key="frame" x="16" y="664" width="186" height="21"/>
<rect key="frame" x="16" y="904" width="186" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="NLH-qN-qCo" userLabel="Statistics Overlay Selector">
<rect key="frame" x="16" y="693" width="209" height="28"/>
<rect key="frame" x="16" y="933" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
<segment title="Yes"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="lcO-6v-Tbg" userLabel="ABXY Swap Selector">
<rect key="frame" x="16" y="544" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -261,19 +322,24 @@
<outlet property="bitrateLabel" destination="lMt-4H-fkV" id="ItM-l3-1Jk"/>
<outlet property="bitrateSlider" destination="JAY-nj-UNz" id="fHd-v5-9Vo"/>
<outlet property="btMouseSelector" destination="KMg-3j-F9p" id="W45-AB-sJf"/>
<outlet property="codecSelector" destination="aFy-0w-YPe" id="DlU-0l-gwz"/>
<outlet property="framePacingSelector" destination="7va-uJ-IfD" id="Tbv-s2-u2k"/>
<outlet property="framerateSelector" destination="lGK-vl-pdw" id="Kc8-Zv-hdm"/>
<outlet property="hevcSelector" destination="aFy-0w-YPe" id="DlU-0l-gwz"/>
<outlet property="hdrSelector" destination="OA2-SA-bBO" id="uUL-mc-4We"/>
<outlet property="multiControllerSelector" destination="KlC-fG-wEi" id="giM-F7-So5"/>
<outlet property="onscreenControlSelector" destination="qSU-wh-tqA" id="j8d-fB-Z2c"/>
<outlet property="optimizeSettingsSelector" destination="nCO-OT-dV1" id="FB0-Rt-C44"/>
<outlet property="resolutionDisplayView" destination="9cG-JD-glb" id="TT1-hy-MUC"/>
<outlet property="resolutionSelector" destination="ckc-Dm-8ex" id="rl6-rx-wd3"/>
<outlet property="scrollView" destination="WRy-3f-gEP" id="V8B-oF-27B"/>
<outlet property="statsOverlaySelector" destination="NLH-qN-qCo" id="EF8-JF-O3S"/>
<outlet property="swapABXYButtonsSelector" destination="lcO-6v-Tbg" id="7VO-yt-0Sp"/>
<outlet property="touchModeSelector" destination="e4G-id-vjI" id="Cwh-fO-Pou"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="01j-TU-OoL" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="608.59375" y="834.9609375"/>
<point key="canvasLocation" x="607.91366906474821" y="834.67336683417091"/>
</scene>
<!--Reveal View Controller-->
<scene sceneID="rR7-ZT-bc7">
@@ -284,7 +350,7 @@
<viewControllerLayoutGuide type="bottom" id="YRO-Lg-RCg"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="hGs-BR-t4b">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
@@ -302,7 +368,7 @@
<objects>
<navigationController id="baW-rW-rBd" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" translucent="NO" id="RUe-14-4Ya">
<rect key="frame" x="0.0" y="0.0" width="1194" height="50"/>
<rect key="frame" x="0.0" y="24" width="834" height="50"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="barTintColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@@ -324,7 +390,7 @@
<viewControllerLayoutGuide type="bottom" id="vu8-3s-HlY"/>
</layoutGuides>
<view key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" id="SMn-1j-R3I">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.3333357871" green="0.33332890269999998" blue="0.33333355190000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</view>
@@ -346,7 +412,7 @@
<viewControllerLayoutGuide type="bottom" id="YZj-An-sqS"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="le1-tU-NEp">
<rect key="frame" x="0.0" y="0.0" width="1194" height="834"/>
<rect key="frame" x="0.0" y="0.0" width="834" height="1194"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="N0D-dj-EuF">
@@ -366,8 +432,11 @@
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="viewFlipsideBackgroundColor">
<color red="0.1215686274509804" green="0.12941176470588239" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+123 -52
View File
@@ -1,9 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="19162" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="DL0-L5-LOv">
<device id="retina6_1" orientation="landscape" appearance="light"/>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="22155" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" colorMatched="YES" initialViewController="DL0-L5-LOv">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="19144"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="22131"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
@@ -12,7 +13,7 @@
<objects>
<viewController id="dgh-JZ-Q7z" customClass="MainFrameViewController" sceneMemberID="viewController">
<collectionView key="view" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" dataMode="prototypes" id="Rtu-AT-Alw" customClass="AppCollectionView">
<rect key="frame" x="0.0" y="0.0" width="896" height="370"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="804"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<collectionViewFlowLayout key="collectionViewLayout" minimumLineSpacing="15" minimumInteritemSpacing="15" id="YcZ-cR-3tK">
@@ -60,7 +61,7 @@
<viewControllerLayoutGuide type="bottom" id="iBq-4E-Dbr"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Usg-e0-g4a">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
@@ -82,37 +83,45 @@
<viewControllerLayoutGuide type="bottom" id="HaZ-6S-ZpN"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="iNk-qF-gIr" customClass="UIScrollView">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Resolution" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="8zy-ri-Dqc">
<rect key="frame" x="16" y="20" width="82" height="21"/>
<rect key="frame" x="22" y="20" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.9513210654258728" green="0.97490358352661133" blue="0.99987185001373291" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="PCM-t4-Sha">
<rect key="frame" x="16" y="49" width="200" height="29"/>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" apportionsSegmentWidthsByContent="YES" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="PCM-t4-Sha">
<rect key="frame" x="16" y="47" width="450" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="360p"/>
<segment title="720p"/>
<segment title="1080p"/>
<segment title="4K"/>
<segment title="Safe Area"/>
<segment title="Full"/>
<segment title="Custom"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Frame Rate" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kkv-DF-MAl">
<rect key="frame" x="16" y="85" width="88" height="21"/>
<view contentMode="scaleToFill" fixedFrame="YES" translatesAutoresizingMaskIntoConstraints="NO" id="Dej-Bj-F6i" userLabel="Resolution Display View">
<rect key="frame" x="15" y="90" width="447" height="35"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Frame Rate" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Kkv-DF-MAl">
<rect key="frame" x="22" y="133" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.9513210654258728" green="0.97490358352661133" blue="0.99987185001373291" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="dLF-qJ-2nY">
<rect key="frame" x="16" y="114" width="200" height="29"/>
<rect key="frame" x="16" y="162" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="30 FPS"/>
@@ -123,26 +132,26 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Bitrate: 10 Mbps" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SBv-Wn-LB7">
<rect key="frame" x="16" y="150" width="151" height="21"/>
<rect key="frame" x="22" y="198" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.9513210654258728" green="0.97490358352661133" blue="0.99987185001373291" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="20" minValue="1" maxValue="100" translatesAutoresizingMaskIntoConstraints="NO" id="3nn-MI-9Xu">
<rect key="frame" x="14" y="179" width="204" height="31"/>
<rect key="frame" x="14" y="227" width="450" height="31"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</slider>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Touch Mode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="CHp-14-aTt">
<rect key="frame" x="17" y="217" width="186" height="21"/>
<rect key="frame" x="22" y="265" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="2lg-HW-tku" userLabel="Touch Mode Selector">
<rect key="frame" x="17" y="245" width="200" height="32"/>
<rect key="frame" x="16" y="293" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Touchpad"/>
@@ -152,14 +161,14 @@
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="On-Screen Controls" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="HZT-dd-DuQ">
<rect key="frame" x="20" y="284" width="153" height="21"/>
<rect key="frame" x="22" y="328" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.9513210654258728" green="0.97490358352661133" blue="0.99987185001373291" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="WGf-9d-eAm">
<rect key="frame" x="20" y="313" width="200" height="29"/>
<rect key="frame" x="16" y="357" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Off"/>
@@ -171,14 +180,14 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Optimize Game Settings" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="hxa-gT-JXC">
<rect key="frame" x="20" y="349" width="186" height="21"/>
<rect key="frame" x="22" y="395" width="218" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="Gob-Lu-b1y" userLabel="Optimize Settings Selector">
<rect key="frame" x="20" y="378" width="200" height="29"/>
<rect key="frame" x="16" y="424" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -188,14 +197,14 @@
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Multi-Controller Mode" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="Jps-kj-bYJ">
<rect key="frame" x="20" y="414" width="169" height="21"/>
<rect key="frame" x="22" y="461" width="218" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="1" translatesAutoresizingMaskIntoConstraints="NO" id="OCT-oL-Dqb" userLabel="MC Selector">
<rect key="frame" x="20" y="443" width="200" height="29"/>
<rect key="frame" x="16" y="490" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Single"/>
@@ -204,15 +213,32 @@
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Play Audio on PC" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T6M-nz-TW0">
<rect key="frame" x="20" y="479" width="131" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Swap A/B and X/Y Buttons" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2p1-r1-Gvf">
<rect key="frame" x="22" y="527" width="218" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="P8R-nu-8Ln" userLabel="ABXY Swap Selector">
<rect key="frame" x="16" y="555" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
<segment title="Yes"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Play Audio on PC" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="T6M-nz-TW0">
<rect key="frame" x="22" y="591" width="143" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="8GA-Js-kQN" userLabel="PC Audio Selector">
<rect key="frame" x="20" y="508" width="200" height="29"/>
<rect key="frame" x="16" y="620" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -221,32 +247,68 @@
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HEVC Video" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BEG-Id-J8y">
<rect key="frame" x="20" y="544" width="93" height="21"/>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Preferred Codec" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="BEG-Id-J8y" userLabel="Preferred Codec">
<rect key="frame" x="22" y="656" width="134" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="AbS-CW-fjP" userLabel="HEVC Selector">
<rect key="frame" x="20" y="573" width="200" height="29"/>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="3" translatesAutoresizingMaskIntoConstraints="NO" id="AbS-CW-fjP" userLabel="Codec Selector">
<rect key="frame" x="16" y="681" width="450" height="32"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="H.264"/>
<segment title="HEVC"/>
<segment title="AV1"/>
<segment title="Auto"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="HDR (Beta)" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="lBe-dn-yc7">
<rect key="frame" x="22" y="719" width="98" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="9xD-GL-9xq" userLabel="HDR Selector">
<rect key="frame" x="16" y="748" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
<segment title="Yes"/>
</segments>
<color key="tintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768741607666" green="0.61711704730987549" blue="0.99902987480163574" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Frame Pacing Preference" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="x1l-m4-eAM">
<rect key="frame" x="22" y="784" width="450" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="d2a-Ka-H2c" userLabel="Frame Pacing Selector">
<rect key="frame" x="16" y="813" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="Lowest Latency"/>
<segment title="Smoothest Video"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Citrix X1 Mouse Support" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="glN-9Q-GKD">
<rect key="frame" x="20" y="609" width="186" height="21"/>
<rect key="frame" x="22" y="849" width="218" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="o4O-hO-teB" userLabel="Citrix X1 Selector">
<rect key="frame" x="20" y="638" width="200" height="29"/>
<rect key="frame" x="16" y="878" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -256,14 +318,14 @@
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Statistics Overlay" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="WtB-ib-ZDj">
<rect key="frame" x="24" y="674" width="186" height="21"/>
<rect key="frame" x="22" y="914" width="208" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="rdz-eg-2oR" userLabel="Statistics Overlay Selector">
<rect key="frame" x="20" y="703" width="200" height="29"/>
<rect key="frame" x="16" y="943" width="450" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
@@ -280,27 +342,31 @@
<outlet property="bitrateLabel" destination="SBv-Wn-LB7" id="Wdu-me-Bvd"/>
<outlet property="bitrateSlider" destination="3nn-MI-9Xu" id="IuD-Rk-vPp"/>
<outlet property="btMouseSelector" destination="o4O-hO-teB" id="bLu-uv-yUX"/>
<outlet property="codecSelector" destination="AbS-CW-fjP" id="ohH-Tc-UzM"/>
<outlet property="framePacingSelector" destination="d2a-Ka-H2c" id="Ocv-3E-lIm"/>
<outlet property="framerateSelector" destination="dLF-qJ-2nY" id="hE3-hk-iwa"/>
<outlet property="hevcSelector" destination="AbS-CW-fjP" id="ohH-Tc-UzM"/>
<outlet property="hdrSelector" destination="9xD-GL-9xq" id="5Rf-R7-5TO"/>
<outlet property="multiControllerSelector" destination="OCT-oL-Dqb" id="bLT-gB-FcH"/>
<outlet property="onscreenControlSelector" destination="WGf-9d-eAm" id="hob-se-4Sn"/>
<outlet property="optimizeSettingsSelector" destination="Gob-Lu-b1y" id="9Wl-yp-0Ou"/>
<outlet property="resolutionDisplayView" destination="Dej-Bj-F6i" id="bi4-N6-snx"/>
<outlet property="resolutionSelector" destination="PCM-t4-Sha" id="t60-W2-wkV"/>
<outlet property="scrollView" destination="iNk-qF-gIr" id="h7U-Xp-ULQ"/>
<outlet property="statsOverlaySelector" destination="rdz-eg-2oR" id="nus-Zq-nwm"/>
<outlet property="swapABXYButtonsSelector" destination="P8R-nu-8Ln" id="anv-Y8-0Mi"/>
<outlet property="touchModeSelector" destination="2lg-HW-tku" id="HrP-Pz-jQp"/>
</connections>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="RWc-Km-KG5" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1974" y="1487"/>
<point key="canvasLocation" x="1973.913043478261" y="1486.6071428571429"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="pfX-8A-htT">
<objects>
<navigationController id="ftZ-kC-fxI" sceneMemberID="viewController">
<navigationBar key="navigationBar" contentMode="scaleToFill" barStyle="black" translucent="NO" id="0ZA-Ec-QgD">
<rect key="frame" x="0.0" y="0.0" width="896" height="44"/>
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
<color key="backgroundColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="barTintColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@@ -325,7 +391,7 @@
<viewControllerLayoutGuide type="bottom" id="kcN-yf-dtN"/>
</layoutGuides>
<view key="view" multipleTouchEnabled="YES" contentMode="scaleToFill" id="p4u-yn-VXk">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<color key="backgroundColor" red="0.3333357871" green="0.33332890269999998" blue="0.33333355190000002" alpha="1" colorSpace="custom" customColorSpace="displayP3"/>
</view>
@@ -347,11 +413,11 @@
<viewControllerLayoutGuide type="bottom" id="hby-2y-jAk"/>
</layoutGuides>
<view key="view" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="center" id="nSw-dc-mi4">
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" animating="YES" style="whiteLarge" translatesAutoresizingMaskIntoConstraints="NO" id="oNu-Gu-QeM">
<rect key="frame" x="266" y="187" width="37" height="37"/>
<rect key="frame" x="266" y="426" width="37" height="37"/>
<autoresizingMask key="autoresizingMask" flexibleMinY="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
</subviews>
@@ -366,4 +432,9 @@
<point key="canvasLocation" x="3031" y="1487"/>
</scene>
</scenes>
<resources>
<systemColor name="systemBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
</resources>
</document>
+5
View File
@@ -0,0 +1,5 @@
1. Pull latest https://github.com/x2on/OpenSSL-for-iPhone
2. Patch build-libssl.sh to adjust IOS_MIN_SDK_VERSION and TVOS_MIN_SDK_VERSION
3. ./build-libssl.sh --disable-bitcode --targets="ios-sim-cross-x86_64 ios-sim-cross-arm64 ios-cross-arm64 tvos-sim-cross-x86_64 tvos-sim-cross-arm64 tvos-cross-arm64"
4. ./create-xcframework.sh
5. Copy OpenSSL.xcframework into moonlight-ios/libs
+1
View File
@@ -0,0 +1 @@
Static libraries were built from https://github.com/cgutman/moonlight-mobile-deps using AppVeyor CI
@@ -0,0 +1,36 @@
/*
* AC-3 parser prototypes
* Copyright (c) 2003 Fabrice Bellard
* Copyright (c) 2003 Michael Niedermayer
*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AC3_PARSER_H
#define AVCODEC_AC3_PARSER_H
#include <stddef.h>
#include <stdint.h>
/**
* Extract the bitstream ID and the frame size from AC-3 data.
*/
int av_ac3_parse_header(const uint8_t *buf, size_t size,
uint8_t *bitstream_id, uint16_t *frame_size);
#endif /* AVCODEC_AC3_PARSER_H */
@@ -0,0 +1,37 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_ADTS_PARSER_H
#define AVCODEC_ADTS_PARSER_H
#include <stddef.h>
#include <stdint.h>
#define AV_AAC_ADTS_HEADER_SIZE 7
/**
* Extract the number of samples and frames from AAC data.
* @param[in] buf pointer to AAC data buffer
* @param[out] samples Pointer to where number of samples is written
* @param[out] frames Pointer to where number of frames is written
* @return Returns 0 on success, error code on failure.
*/
int av_adts_header_parse(const uint8_t *buf, uint32_t *samples,
uint8_t *frames);
#endif /* AVCODEC_ADTS_PARSER_H */
+191
View File
@@ -0,0 +1,191 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
/**
* @file
* AV1 common definitions
*/
#ifndef AVCODEC_AV1_H
#define AVCODEC_AV1_H
// OBU types (section 6.2.2).
typedef enum {
// 0 reserved.
AV1_OBU_SEQUENCE_HEADER = 1,
AV1_OBU_TEMPORAL_DELIMITER = 2,
AV1_OBU_FRAME_HEADER = 3,
AV1_OBU_TILE_GROUP = 4,
AV1_OBU_METADATA = 5,
AV1_OBU_FRAME = 6,
AV1_OBU_REDUNDANT_FRAME_HEADER = 7,
AV1_OBU_TILE_LIST = 8,
// 9-14 reserved.
AV1_OBU_PADDING = 15,
} AV1_OBU_Type;
// Metadata types (section 6.7.1).
enum {
AV1_METADATA_TYPE_HDR_CLL = 1,
AV1_METADATA_TYPE_HDR_MDCV = 2,
AV1_METADATA_TYPE_SCALABILITY = 3,
AV1_METADATA_TYPE_ITUT_T35 = 4,
AV1_METADATA_TYPE_TIMECODE = 5,
};
// Frame types (section 6.8.2).
enum {
AV1_FRAME_KEY = 0,
AV1_FRAME_INTER = 1,
AV1_FRAME_INTRA_ONLY = 2,
AV1_FRAME_SWITCH = 3,
};
// Reference frames (section 6.10.24).
enum {
AV1_REF_FRAME_INTRA = 0,
AV1_REF_FRAME_LAST = 1,
AV1_REF_FRAME_LAST2 = 2,
AV1_REF_FRAME_LAST3 = 3,
AV1_REF_FRAME_GOLDEN = 4,
AV1_REF_FRAME_BWDREF = 5,
AV1_REF_FRAME_ALTREF2 = 6,
AV1_REF_FRAME_ALTREF = 7,
};
// Constants (section 3).
enum {
AV1_MAX_OPERATING_POINTS = 32,
AV1_MAX_SB_SIZE = 128,
AV1_MI_SIZE = 4,
AV1_MAX_TILE_WIDTH = 4096,
AV1_MAX_TILE_AREA = 4096 * 2304,
AV1_MAX_TILE_ROWS = 64,
AV1_MAX_TILE_COLS = 64,
AV1_NUM_REF_FRAMES = 8,
AV1_REFS_PER_FRAME = 7,
AV1_TOTAL_REFS_PER_FRAME = 8,
AV1_PRIMARY_REF_NONE = 7,
AV1_MAX_SEGMENTS = 8,
AV1_SEG_LVL_MAX = 8,
AV1_SEG_LVL_ALT_Q = 0,
AV1_SEG_LVL_ALT_LF_Y_V = 1,
AV1_SEG_LVL_REF_FRAME = 5,
AV1_SEG_LVL_SKIP = 6,
AV1_SEG_LVL_GLOBAL_MV = 7,
AV1_SELECT_SCREEN_CONTENT_TOOLS = 2,
AV1_SELECT_INTEGER_MV = 2,
AV1_SUPERRES_NUM = 8,
AV1_SUPERRES_DENOM_MIN = 9,
AV1_INTERPOLATION_FILTER_SWITCHABLE = 4,
AV1_GM_ABS_ALPHA_BITS = 12,
AV1_GM_ALPHA_PREC_BITS = 15,
AV1_GM_ABS_TRANS_ONLY_BITS = 9,
AV1_GM_TRANS_ONLY_PREC_BITS = 3,
AV1_GM_ABS_TRANS_BITS = 12,
AV1_GM_TRANS_PREC_BITS = 6,
AV1_WARPEDMODEL_PREC_BITS = 16,
AV1_WARP_MODEL_IDENTITY = 0,
AV1_WARP_MODEL_TRANSLATION = 1,
AV1_WARP_MODEL_ROTZOOM = 2,
AV1_WARP_MODEL_AFFINE = 3,
AV1_WARP_PARAM_REDUCE_BITS = 6,
AV1_DIV_LUT_BITS = 8,
AV1_DIV_LUT_PREC_BITS = 14,
AV1_DIV_LUT_NUM = 257,
AV1_MAX_LOOP_FILTER = 63,
};
// The main colour configuration information uses the same ISO/IEC 23001-8
// (H.273) enums as FFmpeg does, so separate definitions are not required.
// Chroma sample position.
enum {
AV1_CSP_UNKNOWN = 0,
AV1_CSP_VERTICAL = 1, // -> AVCHROMA_LOC_LEFT.
AV1_CSP_COLOCATED = 2, // -> AVCHROMA_LOC_TOPLEFT.
};
// Scalability modes (section 6.7.5)
enum {
AV1_SCALABILITY_L1T2 = 0,
AV1_SCALABILITY_L1T3 = 1,
AV1_SCALABILITY_L2T1 = 2,
AV1_SCALABILITY_L2T2 = 3,
AV1_SCALABILITY_L2T3 = 4,
AV1_SCALABILITY_S2T1 = 5,
AV1_SCALABILITY_S2T2 = 6,
AV1_SCALABILITY_S2T3 = 7,
AV1_SCALABILITY_L2T1h = 8,
AV1_SCALABILITY_L2T2h = 9,
AV1_SCALABILITY_L2T3h = 10,
AV1_SCALABILITY_S2T1h = 11,
AV1_SCALABILITY_S2T2h = 12,
AV1_SCALABILITY_S2T3h = 13,
AV1_SCALABILITY_SS = 14,
AV1_SCALABILITY_L3T1 = 15,
AV1_SCALABILITY_L3T2 = 16,
AV1_SCALABILITY_L3T3 = 17,
AV1_SCALABILITY_S3T1 = 18,
AV1_SCALABILITY_S3T2 = 19,
AV1_SCALABILITY_S3T3 = 20,
AV1_SCALABILITY_L3T2_KEY = 21,
AV1_SCALABILITY_L3T3_KEY = 22,
AV1_SCALABILITY_L4T5_KEY = 23,
AV1_SCALABILITY_L4T7_KEY = 24,
AV1_SCALABILITY_L3T2_KEY_SHIFT = 25,
AV1_SCALABILITY_L3T3_KEY_SHIFT = 26,
AV1_SCALABILITY_L4T5_KEY_SHIFT = 27,
AV1_SCALABILITY_L4T7_KEY_SHIFT = 28,
};
// Frame Restoration types (section 6.10.15)
enum {
AV1_RESTORE_NONE = 0,
AV1_RESTORE_WIENER = 1,
AV1_RESTORE_SGRPROJ = 2,
AV1_RESTORE_SWITCHABLE = 3,
};
// TX mode (section 6.8.21)
enum {
AV1_ONLY_4X4 = 0,
AV1_TX_MODE_LARGEST = 1,
AV1_TX_MODE_SELECT = 2,
};
// Sequence Headers are actually unbounded because one can use
// an arbitrary number of leading zeroes when encoding via uvlc.
// The following estimate is based around using the lowest number
// of bits for uvlc encoding.
#define AV1_SANE_SEQUENCE_HEADER_MAX_BITS 3138
#endif /* AVCODEC_AV1_H */
File diff suppressed because it is too large Load Diff
+88
View File
@@ -0,0 +1,88 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AVDCT_H
#define AVCODEC_AVDCT_H
#include "libavutil/opt.h"
/**
* AVDCT context.
* @note function pointers can be NULL if the specific features have been
* disabled at build time.
*/
typedef struct AVDCT {
const AVClass *av_class;
void (*idct)(int16_t *block /* align 16 */);
/**
* IDCT input permutation.
* Several optimized IDCTs need a permutated input (relative to the
* normal order of the reference IDCT).
* This permutation must be performed before the idct_put/add.
* Note, normally this can be merged with the zigzag/alternate scan<br>
* An example to avoid confusion:
* - (->decode coeffs -> zigzag reorder -> dequant -> reference IDCT -> ...)
* - (x -> reference DCT -> reference IDCT -> x)
* - (x -> reference DCT -> simple_mmx_perm = idct_permutation
* -> simple_idct_mmx -> x)
* - (-> decode coeffs -> zigzag reorder -> simple_mmx_perm -> dequant
* -> simple_idct_mmx -> ...)
*/
uint8_t idct_permutation[64];
void (*fdct)(int16_t *block /* align 16 */);
/**
* DCT algorithm.
* must use AVOptions to set this field.
*/
int dct_algo;
/**
* IDCT algorithm.
* must use AVOptions to set this field.
*/
int idct_algo;
void (*get_pixels)(int16_t *block /* align 16 */,
const uint8_t *pixels /* align 8 */,
ptrdiff_t line_size);
int bits_per_sample;
void (*get_pixels_unaligned)(int16_t *block /* align 16 */,
const uint8_t *pixels,
ptrdiff_t line_size);
} AVDCT;
/**
* Allocates a AVDCT context.
* This needs to be initialized with avcodec_dct_init() after optionally
* configuring it with AVOptions.
*
* To free it use av_free()
*/
AVDCT *avcodec_dct_alloc(void);
int avcodec_dct_init(AVDCT *);
const AVClass *avcodec_dct_get_class(void);
#endif /* AVCODEC_AVDCT_H */
+149
View File
@@ -0,0 +1,149 @@
/*
* This file is part of FFmpeg.
*
* FFmpeg is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* FFmpeg is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with FFmpeg; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
#ifndef AVCODEC_AVFFT_H
#define AVCODEC_AVFFT_H
#include "libavutil/attributes.h"
#include "version_major.h"
#if FF_API_AVFFT
/**
* @file
* @ingroup lavc_fft
* FFT functions
*/
/**
* @defgroup lavc_fft FFT functions
* @ingroup lavc_misc
*
* @{
*/
typedef float FFTSample;
typedef struct FFTComplex {
FFTSample re, im;
} FFTComplex;
typedef struct FFTContext FFTContext;
/**
* Set up a complex FFT.
* @param nbits log2 of the length of the input array
* @param inverse if 0 perform the forward transform, if 1 perform the inverse
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_FFT
*/
attribute_deprecated
FFTContext *av_fft_init(int nbits, int inverse);
/**
* Do the permutation needed BEFORE calling ff_fft_calc().
* @deprecated without replacement
*/
attribute_deprecated
void av_fft_permute(FFTContext *s, FFTComplex *z);
/**
* Do a complex FFT with the parameters defined in av_fft_init(). The
* input data must be permuted before. No 1.0/sqrt(n) normalization is done.
* @deprecated use the av_tx_fn value returned by av_tx_init, which also does permutation
*/
attribute_deprecated
void av_fft_calc(FFTContext *s, FFTComplex *z);
attribute_deprecated
void av_fft_end(FFTContext *s);
/**
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_MDCT,
* with a flag of AV_TX_FULL_IMDCT for a replacement to av_imdct_calc.
*/
attribute_deprecated
FFTContext *av_mdct_init(int nbits, int inverse, double scale);
attribute_deprecated
void av_imdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_imdct_half(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_mdct_calc(FFTContext *s, FFTSample *output, const FFTSample *input);
attribute_deprecated
void av_mdct_end(FFTContext *s);
/* Real Discrete Fourier Transform */
enum RDFTransformType {
DFT_R2C,
IDFT_C2R,
IDFT_R2C,
DFT_C2R,
};
typedef struct RDFTContext RDFTContext;
/**
* Set up a real FFT.
* @param nbits log2 of the length of the input array
* @param trans the type of transform
*
* @deprecated use av_tx_init from libavutil/tx.h with a type of AV_TX_FLOAT_RDFT
*/
attribute_deprecated
RDFTContext *av_rdft_init(int nbits, enum RDFTransformType trans);
attribute_deprecated
void av_rdft_calc(RDFTContext *s, FFTSample *data);
attribute_deprecated
void av_rdft_end(RDFTContext *s);
/* Discrete Cosine Transform */
typedef struct DCTContext DCTContext;
enum DCTTransformType {
DCT_II = 0,
DCT_III,
DCT_I,
DST_I,
};
/**
* Set up DCT.
*
* @param nbits size of the input array:
* (1 << nbits) for DCT-II, DCT-III and DST-I
* (1 << nbits) + 1 for DCT-I
* @param type the type of transform
*
* @note the first element of the input of DST-I is ignored
*
* @deprecated use av_tx_init from libavutil/tx.h with an appropriate type of AV_TX_FLOAT_DCT
*/
attribute_deprecated
DCTContext *av_dct_init(int nbits, enum DCTTransformType type);
attribute_deprecated
void av_dct_calc(DCTContext *s, FFTSample *data);
attribute_deprecated
void av_dct_end (DCTContext *s);
/**
* @}
*/
#endif /* FF_API_AVFFT */
#endif /* AVCODEC_AVFFT_H */

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