Compare commits

..
319 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
Cameron Gutman 8391c766c7 Switch to SDL for audio output 2022-01-13 22:00:47 -06:00
Cameron Gutman eccf517dc3 Fix scroll speed on recent GFE versions 2021-12-12 16:33:19 -06:00
Cameron Gutman 90d416ab34 Make GCMouse scroll direction consistent with UIPanGestureRecognizer 2021-12-12 16:28:21 -06:00
Cameron Gutman 283a5516d8 Fix erratic scroll movement when beginning a scroll gesture 2021-12-12 16:16:46 -06:00
Cameron Gutman dee18fb5c2 Add building instructions 2021-10-16 14:18:04 -05:00
Cameron Gutman c99ee24c65 Restore GCMouse scroll handling for tvOS 2021-10-15 22:32:32 -05:00
Cameron Gutman 76ef2ed432 Version 7.2.0 2021-10-12 19:27:46 -05:00
Cameron Gutman 92a2bbe28e Send Command key to host rather than treating it as Control
Fixes #464
2021-10-12 19:01:14 -05:00
Cameron Gutman 3dd0e8362a Xcode plist reorganization 2021-10-06 23:39:23 -05:00
Cameron Gutman cea8ed485f Improve scrolling behavior for continuous scroll input 2021-10-06 23:28:29 -05:00
Cameron Gutman 505d248472 Improve scrolling behavior for discrete scroll input 2021-10-06 23:20:42 -05:00
Cameron Gutman 3722de4011 Fix navigation bar color when building with iOS 15 SDK 2021-10-05 22:28:41 -05:00
Cameron Gutman 3f530afca4 Add CADisableMinimumFrameDurationOnPhone for iPhone 120 Hz support 2021-10-05 21:43:54 -05:00
Cameron Gutman d7cf8ced25 Go back to UIPanGestureRecognizer for all scroll events
GCMouse is broken in iPad 15 for mice with discrete scroll wheel events
2021-10-05 21:42:37 -05:00
Cameron Gutman c930a4656a Switch to X1Kit fork with Xcode 13 fixes 2021-10-05 21:08:10 -05:00
Cameron Gutman e80a4fd2b1 Add support for Unicode text input 2021-08-10 01:01:51 -05:00
Cameron Gutman 68e6690e55 Version 7.1.3 b2 2021-07-09 23:23:51 -05:00
Cameron Gutman 6c45a5c43a Actually update the moonlight-common-c submodule 2021-07-09 23:20:14 -05:00
Cameron Gutman 6490bb6d67 Version 7.1.3 2021-07-09 23:18:29 -05:00
Cameron Gutman 93bcd8615c Update moonlight-common-c to remove SEI NALs 2021-07-09 23:05:25 -05:00
Cameron Gutman 0fe84a7a1e Remove key frame detection logic
Moonlight-common-c already tells us whether a frame is an IDR or not
2021-07-09 17:43:36 -05:00
Cameron Gutman 7ed6b22cac Pass RTSP session URL to moonlight-common-c for dynamic ports 2021-07-02 17:31:45 -05:00
Cameron Gutman 295cf61727 Version 7.1.2 2021-06-20 17:14:00 -05:00
Cameron Gutman c234c82e63 Update moonlight-common-c with performance improvements and legacy GFE compatibility 2021-06-20 16:48:40 -05:00
Cameron Gutman e143dff76a Version 7.1.1 2021-06-04 18:54:51 -05:00
Cameron Gutman d317cdc476 Update moonlight-common-c with audio FEC support 2021-06-03 21:19:01 -05:00
Cameron Gutman b29259a330 Version 7.1.0 2021-05-19 22:20:53 -05:00
Cameron Gutman f804d000f9 Update moonlight-common-c to avoid excessive ENet retransmissions when RTT variance is 0 2021-05-16 15:42:20 -05:00
Cameron Gutman 9baaa10417 Update moonlight-common-c with with more accurate RTTs and minRequiredFecPackets 2021-05-16 14:51:17 -05:00
Cameron Gutman 073873f681 Add network latency to stats overlay 2021-05-15 17:22:46 -05:00
Cameron Gutman ab5a0a4470 Version 7.0.8 2021-05-06 23:57:36 -05:00
Cameron Gutman fd1db224cb Update moonlight-common-c with audio latency fix 2021-05-06 23:56:38 -05:00
Cameron Gutman 2d8d176160 Disable reference frame invalidation on tvOS and always enable HEVC (for now)
Reference frame invalidation seems to trigger a renderer hang on tvOS 14.5 in my testing.
Since we don't have RFI to bias us toward H.264, let's use HEVC all the time now.

This may change again if the RFI issues are sorted out (perhaps we need to implement
blocking waits for invalidated frames?)
2021-05-06 23:56:08 -05:00
Cameron Gutman a4035c18b5 Switch stream exit button to Play/Pause on Apple TV
It appears it's no longer possible to get a double-press for the Menu button on tvOS 14.5

Fixes #456
2021-05-06 23:42:52 -05:00
Cameron Gutman 05aedd5510 Version 7.0.7 2021-04-29 20:17:58 -05:00
Cameron Gutman fd5426b7e8 Update moonlight-common-c with RTSP handshake retry logic 2021-04-29 18:47:24 -05:00
Cameron Gutman 651559727f Version 7.0.6 2021-04-27 18:41:23 -05:00
Cameron Gutman 76751dbee2 Update moonlight-common-c with multi-FEC support 2021-04-27 18:37:09 -05:00
Cameron Gutman 1c3d072b49 Add audio stream encryption 2021-04-23 19:07:27 -05:00
Cameron Gutman d88321263f Update moonlight-common-c 2021-04-18 19:18:42 -05:00
Cameron Gutman 867777468f Version 7.0.5 2021-04-09 19:48:14 -05:00
Cameron Gutman d52703c20e Update moonlight-common-c for initial GFE 3.22 compatibility 2021-04-09 19:37:27 -05:00
Cameron Gutman 3e90e2ebf2 Version 7.0.4 2021-04-03 13:22:09 -05:00
Cameron Gutman 16acd71f6a Disable system gestures on gamepad buttons while streaming 2021-04-03 12:54:08 -05:00
Cameron Gutman 4079af5d22 Update moonlight-common-c to pick up AMF HEVC parsing fix 2021-03-31 20:06:03 -05:00
Cameron Gutman 97f96bb116 Migrate to AppVeyor 2021-03-03 17:14:20 -06:00
Cameron Gutman 81b9a3e859 Migrate from travis-ci.org to travis-ci.com 2021-03-03 01:54:31 -06:00
Cameron Gutman 5da55d88c5 Update template to direct to troubleshooting page 2021-03-03 01:39:31 -06:00
Cameron Gutman 1fdbd84cd4 Add issue templates 2021-03-03 01:21:55 -06:00
Cameron Gutman a66d6eb11b Version 7.0.3 2021-02-02 18:50:31 -06:00
Cameron Gutman d92437cfc9 Update moonlight-common-c 2021-02-02 18:05:01 -06:00
Cameron Gutman f719e080f9 Merge pull request #448 from ascagnel/tweak-tvos-icons
tweak tvOS icons
2021-02-02 18:00:48 -06:00
Cameron Gutman b7740eaffd Merge pull request #447 from mlostekk/fix/memLeaks
nil'ing observer references
2021-02-02 18:00:30 -06:00
Andrew Scagnelli 7b89d3813b tweaking gradient 2021-01-24 11:48:18 -05:00
Andrew Scagnelli b51cd67327 tweak tvOS icons
add a gradient background so the app icon fits in a little better with
tvOS 14
2021-01-24 11:33:56 -05:00
Martin Mlostek 6e480a46a0 nil'ing observer references 2021-01-23 22:05:58 +01:00
Cameron Gutman a3fe8e23f1 Enable some restricted key combos to pass through to the PC 2021-01-12 18:44:32 -06:00
Cameron Gutman e76344812e Don't lock enhancement requests 2021-01-01 13:21:40 -06:00
Cameron Gutman eed2b82230 Add configuration for GitHub lock bot 2021-01-01 13:08:42 -06:00
Cameron Gutman ecd3427767 Add handling for above 4K resolutions 2020-12-24 13:33:54 -06:00
Cameron Gutman 311bf33e84 Add special error text for an unexpected early termination 2020-12-24 12:48:10 -06:00
Cameron Gutman ea96bac31a Display failing ports when connection fails 2020-12-23 14:48:20 -06:00
Cameron Gutman f09b2fd222 Use LiStringifyPortFlags() rather than coding it ourselves 2020-12-23 14:39:51 -06:00
Cameron Gutman 8b8bc2f818 Version 7.0.2 (iOS) 2020-12-18 17:10:29 -06:00
Cameron Gutman 949f088ef5 Fix dragging mouse on iOS 13
Fixes #440
2020-12-18 17:10:16 -06:00
Cameron Gutman e29313d9b6 Version 7.0.1 (iOS) 2020-12-07 21:45:55 -06:00
Cameron Gutman 9e669d13fa Suppress all kbProductivity gesture recognizers 2020-12-07 21:10:30 -06:00
Cameron Gutman 015cfecdd6 Fix 3 finger taps causing the editing toolbar to appear 2020-12-07 20:54:24 -06:00
Cameron Gutman c623d40600 Update moonlight-common-c submodule 2020-12-07 20:08:31 -06:00
Cameron Gutman 65293dab30 Fix race condition during PC detection 2020-12-06 00:56:16 -06:00
Cameron Gutman f3635a515c Version 7.0.1 (tvOS) 2020-12-05 21:06:50 -06:00
Cameron Gutman 6b823e155a Don't emulate an OSC controller if absolute touch mode is enabled 2020-12-05 20:49:35 -06:00
Cameron Gutman 03f330ccff Fix keyboard input on tvOS
Fixes #434
2020-12-05 20:48:58 -06:00
Cameron Gutman c301891ff5 Fix stats overlay being off by 1 FPS 2020-12-05 20:38:30 -06:00
Cameron Gutman 1ba1ad64ec Version 7.0.0 2020-11-28 22:32:01 -06:00
Cameron Gutman 1f24d28ddd Update quit tip text for Apple TV 2020-11-28 20:23:15 -06:00
Cameron Gutman e9e03c74e2 Fix long pressing apps on tvOS 2020-11-28 20:16:26 -06:00
Cameron Gutman 0c6c3ccc83 Disable resizing the view for the keyboard size 2020-11-28 18:50:54 -06:00
Cameron Gutman 07b8e7fd50 Minor UI tweaks 2020-11-28 18:41:44 -06:00
Cameron Gutman 60a6582380 Fix conflicting touch action handling on UIAppView and UIComputerView 2020-11-14 17:03:22 -06:00
Cameron Gutman f5e0443abb Hide shadows on hidden app tiles 2020-11-14 16:46:00 -06:00
Cameron Gutman 9b8a179f4a Perform the initial updateLoop iteration immediately after attaching the view 2020-11-14 16:42:17 -06:00
Cameron Gutman 78f2b8ac47 Enable right-click context menus on iPadOS 2020-11-14 15:57:16 -06:00
Cameron Gutman dc9b5b7c96 Remove @available checks for < iOS 9.3 2020-11-14 15:27:49 -06:00
Cameron Gutman 313af5a7e7 Fix popover dialog not correctly anchoring to the UIComputerView on iPad
The view was being replaced after the UIAlertController was anchored to it
2020-11-14 15:15:46 -06:00
Cameron Gutman bb885465f6 Merge pull request #429 from MichaelMKenny/ipad-alertcontroller-crash-fix
Fixed crash on iPad due to missing up UIAlertController popoverPresentation.
2020-11-14 14:58:54 -06:00
Michael Kenny e258551008 Fixed crash on iPad when bringing up app long press menus, due to missing UIAlertController popoverPresentation. 2020-11-04 14:11:27 +10:30
Cameron Gutman e402902d6e Fix tvOS build 2020-11-02 20:39:37 -06:00
Cameron Gutman f55daf941c Add support for hiding apps 2020-11-02 20:39:24 -06:00
Cameron Gutman a483c6ea41 Allow panning to the bottom of the screen when the keyboard appears 2020-11-02 18:41:38 -06:00
Cameron Gutman 3ad4d857e8 Replace home-rolled edge swipe gesture recognizer with UIScreenEdgePanGestureRecognizer 2020-11-01 21:18:06 -06:00
Cameron Gutman 6b1d34e4a9 StreamView must be multi-touch enabled for gestures to work 2020-11-01 20:50:25 -06:00
Cameron Gutman bf36eaf661 Improve soft-keyboard handling 2020-11-01 20:47:14 -06:00
Cameron Gutman e809afdd9e Implement zoom and pan in touchscreen mode 2020-11-01 20:22:01 -06:00
Cameron Gutman 0d75dd4efb Create stream view hierarchy programmatically 2020-11-01 20:00:39 -06:00
Cameron Gutman b799978cac Remove old iOS 13.0 gesture recognizer hack 2020-11-01 17:53:21 -06:00
Cameron Gutman 42f29c44e6 Remove OSPortabilityDefs.h
The macOS support that used it has been removed for a while
2020-11-01 16:50:02 -06:00
Cameron Gutman c3ba447372 Reorganize tvOS settings 2020-11-01 16:43:13 -06:00
Cameron Gutman a358cdad3d Improve behavior of 2 finger scrolling in trackpad mode 2020-11-01 16:02:34 -06:00
Cameron Gutman a1f09f117f Fix first tap in touch mode not registering 2020-11-01 14:43:55 -06:00
Cameron Gutman 7270554153 Scale dead zone areas by screen size 2020-11-01 14:36:43 -06:00
Cameron Gutman 8d127decb6 Shrink and deadzone the edge swipe region for touchscreen mode 2020-11-01 14:28:13 -06:00
Cameron Gutman 8ffee9e10f Minor video stats improvements 2020-11-01 14:09:05 -06:00
Cameron Gutman 0eac28a74f Fix stats overlay width decreasing each iteration 2020-11-01 13:45:24 -06:00
Cameron Gutman 5d94800e11 Update moonlight-common-c to disable QoS on IPv6 2020-11-01 13:27:40 -06:00
Cameron Gutman 44f713f5c9 Add a basic stats overlay 2020-11-01 13:27:10 -06:00
Cameron Gutman 2114e39237 Plumb touch mode and stats overlay preferences 2020-11-01 11:50:00 -06:00
Cameron Gutman 6fd8baee41 Implement AbsoluteTouchHandler 2020-11-01 11:07:42 -06:00
Cameron Gutman 63beaebe55 Stub AbsoluteTouchHandler 2020-11-01 10:30:39 -06:00
Cameron Gutman 17d4079a5a Refactor relative touch handling into a separate class 2020-11-01 10:19:32 -06:00
Cameron Gutman b650119fe9 Merge pull request #426 from nauxliu/bump-max-bitrate
Bump max bitrate to 150Mbps to line up with android and qt version
2020-10-28 20:56:55 -05:00
Cameron Gutman 8b28606952 Update moonlight-common-c with QoS fix 2020-10-28 20:44:43 -05:00
Xiangxuan Liu 24258d6e17 Bump max bitrate to 150Mbps to line up with android and qt version 2020-10-28 01:04:34 +08:00
Cameron Gutman 3f1699258d Increase inactivity threshold to 60 seconds before killing the stream 2020-10-18 17:44:52 -05:00
Cameron Gutman 34f8696a5e Don't terminate the stream when resigning active on tvOS 2020-10-18 17:43:34 -05:00
Cameron Gutman 96e317fe31 Make Guide button emulation more granular by only enabling RB+Start or Start+Select based on physical buttons present 2020-10-18 17:37:18 -05:00
Cameron Gutman ecad1bea38 Disable Guide button emulation on gamepads with physical Guide buttons 2020-10-18 15:04:09 -05:00
Cameron Gutman e4534fd4af Version 6.1.1 2020-10-17 22:17:00 -05:00
Cameron Gutman 126735fa57 Update moonlight-common-c with MTU test 2020-10-17 21:52:31 -05:00
Cameron Gutman 9ff5fcb9d5 Fix off by one in rumble amplitude calculation 2020-10-17 21:25:10 -05:00
Cameron Gutman 8c66898765 Dynamically update haptic effects instead of creating new players all the time 2020-10-15 18:40:13 -05:00
Cameron Gutman 5faf1faf32 Add Guide button support on iOS 14 2020-10-15 17:55:53 -05:00
Cameron Gutman 7e704ee201 Trim the IP address string from the user 2020-10-06 21:57:22 -05:00
Cameron Gutman c3ae83f902 Version 6.1.0 2020-09-18 22:41:03 -05:00
Cameron Gutman 95e16c5e2d Sort ITSAppUsesNonExemptEncryption 2020-09-18 21:54:05 -05:00
Cameron Gutman fc08a7ee0e Set ITSAppUsesNonExemptEncryption=NO 2020-09-18 19:47:25 -05:00
Cameron Gutman 95e12f9853 Fix sorting of NSLocalNetworkUsageDescription 2020-09-18 19:18:51 -05:00
Cameron Gutman 88c18ad397 Don't use pointer lock if the mouse isn't compatible with GCMouse 2020-09-18 18:48:35 -05:00
Cameron Gutman 5043fadace Request pointer lock while streaming 2020-09-18 18:27:20 -05:00
Cameron Gutman 6fa5ef73f6 Add GCMouse support 2020-09-18 17:04:32 -05:00
Cameron Gutman c0367f711b Disable legacy iOS 13.4 mouse support when a GCMouse is connected 2020-09-18 16:44:24 -05:00
Cameron Gutman 0a5499f369 Raise iOS deployment target to iOS 9.3 as required by Xcode 12 2020-09-18 16:29:46 -05:00
Cameron Gutman 26209f187e Update TravisCI to use Xcode 12 2020-09-18 15:30:21 -05:00
Cameron Gutman e8c7eb67c6 Add rumble support on iOS 14 2020-09-17 20:32:41 -05:00
Cameron Gutman 2d24b0ec7b Fix mouse support on iOS 14 2020-09-17 18:59:18 -05:00
480 changed files with 92102 additions and 35903 deletions
+48
View File
@@ -0,0 +1,48 @@
---
name: Bug report
about: Follow the troubleshooting guide before reporting a bug
---
**READ ME FIRST!**
If you're here because something basic is not working (like gamepad input, video, or similar), it's probably something specific to your setup, so make sure you've gone through the Troubleshooting Guide first: https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting
If you still have trouble with basic functionality after following the guide, join our Discord server where there are many other volunteers who can help (or direct you back here if it looks like a Moonlight bug after all). https://moonlight-stream.org/discord
**Describe the bug**
A clear and concise description of what the bug is.
**Steps to reproduce**
Any special steps that are required for the bug to appear.
**Screenshots**
If applicable, add screenshots to help explain your problem. If the issue is related to video glitching or poor quality, please include screenshots.
**Affected games**
List the games you've tried that exhibit the issue. To see if the issue is game-specific, try streaming Steam Big Picture with Moonlight and see if the issue persists there.
**Other Moonlight clients**
- Does the issue occur when using Moonlight on PC or Android?
**Moonlight settings (please complete the following information)**
- Have any settings been adjusted from defaults?
- If so, which settings have been changed?
- Does the problem still occur after reverting settings back to default?
**Gamepad-related issues (please complete if problem is gamepad-related)**
- Do you have any gamepads connected to your host PC directly?
- If gamepad input is not working, does it work if you use Moonlight's on-screen controls?
- Does the problem still remain if you stream the desktop and use https://html5gamepad.com to test your gamepad?
- Instructions for streaming the desktop can be found here: https://github.com/moonlight-stream/moonlight-docs/wiki/Setup-Guide
**Device details (please complete the following information)**
- iOS/tvOS version: [e.g. iOS 14.2]
- Device model: [e.g. iPhone 11 Pro]
**Server PC details (please complete the following information)**
- OS: [e.g. Windows 10 1809]
- GeForce Experience version: [e.g. 3.16.0.140]
- Nvidia GPU driver: [e.g. 417.35]
- Antivirus and firewall software: [e.g. Windows Defender and Windows Firewall]
**Additional context**
Anything else you think may be relevant to the issue or special about your specific setup.
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+17
View File
@@ -0,0 +1,17 @@
---
name: Feature request
about: Suggest an idea for this project
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+25
View File
@@ -0,0 +1,25 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads-app
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 60
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels:
- enhancement
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: false
# Comment to post before locking. Set to `false` to disable
lockComment: >
This issue has been automatically locked since there has not been
any recent activity after it was closed. If you experience an issue
that looks similar to this one, please open a new issue with details
about your specific streaming setup.
# Don't lock PRs
only: issues
+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
+3
View File
@@ -1,3 +1,6 @@
[submodule "moonlight-common/moonlight-common-c"]
path = moonlight-common/moonlight-common-c
url = https://github.com/moonlight-stream/moonlight-common-c.git
[submodule "X1Kit"]
path = X1Kit
url = https://github.com/cgutman/X1Kit.git
-25
View File
@@ -1,25 +0,0 @@
language: objective-c
git:
depth: 1
matrix:
include:
- os: osx
osx_image: xcode11.5
env:
- SCHEME="Moonlight"
- TARGET="platform=iOS Simulator,OS=13.5,name=iPhone 11 Pro"
- os: osx
osx_image: xcode11.5
env:
- SCHEME="Moonlight"
- TARGET="platform=iOS Simulator,OS=13.5,name=iPad Pro (9.7-inch)"
- os: osx
osx_image: xcode11.5
env:
- SCHEME="Moonlight TV"
- TARGET="platform=tvOS Simulator,OS=13.4,name=Apple TV 4K"
script:
- set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -destination "$TARGET" build | xcpretty
-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.
+3 -5
View File
@@ -24,11 +24,9 @@ static NSString* DB_NAME = @"Limelight_iOS.sqlite";
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
#if !TARGET_OS_TV
if (@available(iOS 9.0, *)) {
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
if (shortcut != nil) {
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
}
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
if (shortcut != nil) {
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
}
#endif
return YES;
+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};
}
+7 -2
View File
@@ -17,13 +17,18 @@
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
absoluteTouchMode:(BOOL)absoluteTouchMode;
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
statsOverlay:(BOOL)statsOverlay;
- (NSArray*) getHosts;
- (void) updateHost:(TemporaryHost*)host;
+15 -6
View File
@@ -21,12 +21,11 @@
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
// Main Thread Checker happy.
if ([NSThread isMainThread]) {
_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
}
else {
dispatch_sync(dispatch_get_main_queue(), ^{
self->_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
self->_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
});
}
@@ -57,13 +56,18 @@
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
absoluteTouchMode:(BOOL)absoluteTouchMode {
btMouseSupport:(BOOL)btMouseSupport
absoluteTouchMode:(BOOL)absoluteTouchMode
statsOverlay:(BOOL)statsOverlay {
[_managedObjectContext performBlockAndWait:^{
Settings* settingsToSave = [self retrieveSettings];
@@ -71,13 +75,18 @@
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;
settingsToSave.statsOverlay = statsOverlay;
[self saveData];
}];
+1
View File
@@ -15,6 +15,7 @@
@property (nullable, nonatomic, retain) NSString *name;
@property (nullable, nonatomic, retain) NSString *installPath;
@property (nonatomic) BOOL hdrSupported;
@property (nonatomic) BOOL hidden;
@property (nullable, nonatomic, retain) TemporaryHost *host;
NS_ASSUME_NONNULL_BEGIN
+2
View File
@@ -16,6 +16,7 @@
self.id = app.id;
self.name = app.name;
self.hdrSupported = app.hdrSupported;
self.hidden = app.hidden;
self.host = tempHost;
return self;
@@ -25,6 +26,7 @@
parent.id = self.id;
parent.name = self.name;
parent.hdrSupported = self.hdrSupported;
parent.hidden = self.hidden;
parent.host = host;
}
+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;
+11 -2
View File
@@ -16,15 +16,24 @@
@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 streamingRemotely;
@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;
@property (nonatomic) BOOL btMouseSupport;
@property (nonatomic) BOOL absoluteTouchMode;
@property (nonatomic) BOOL statsOverlay;
- (id) initFromSettings:(Settings*)settings;
+17 -2
View File
@@ -34,11 +34,17 @@
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"];
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
switch (_screenSize) {
@@ -54,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();
}
@@ -63,13 +73,18 @@
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;
self.statsOverlay = settings.statsOverlay;
#endif
self.uniqueId = settings.uniqueId;
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

+19
View File
@@ -0,0 +1,19 @@
//
// AbsoluteTouchHandler.h
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "StreamView.h"
NS_ASSUME_NONNULL_BEGIN
@interface AbsoluteTouchHandler : UIResponder
-(id)initWithView:(StreamView*)view;
@end
NS_ASSUME_NONNULL_END
+120
View File
@@ -0,0 +1,120 @@
//
// AbsoluteTouchHandler.m
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "AbsoluteTouchHandler.h"
#include <Limelight.h>
// How long the fingers must be stationary to start a right click
#define LONG_PRESS_ACTIVATION_DELAY 0.650f
// How far the finger can move before it cancels a right click
#define LONG_PRESS_ACTIVATION_DELTA 0.01f
// How long the double tap deadzone stays in effect between touch up and touch down
#define DOUBLE_TAP_DEAD_ZONE_DELAY 0.250f
// How far the finger can move before it can override the double tap deadzone
#define DOUBLE_TAP_DEAD_ZONE_DELTA 0.025f
@implementation AbsoluteTouchHandler {
StreamView* view;
NSTimer* longPressTimer;
UITouch* lastTouchDown;
CGPoint lastTouchDownLocation;
UITouch* lastTouchUp;
CGPoint lastTouchUpLocation;
}
- (id)initWithView:(StreamView*)view {
self = [self init];
self->view = view;
return self;
}
- (void)onLongPressStart:(NSTimer*)timer {
// Raise the left click and start a right click
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
// Ignore touch down events with more than one finger
if ([[event allTouches] count] > 1) {
return;
}
UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:view];
// Don't reposition for finger down events within the deadzone. This makes double-clicking easier.
if (touch.timestamp - lastTouchUp.timestamp > DOUBLE_TAP_DEAD_ZONE_DELAY ||
sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchUpLocation.x / view.bounds.size.width), 2) +
pow((touchLocation.y / view.bounds.size.height) - (lastTouchUpLocation.y / view.bounds.size.height), 2)) > DOUBLE_TAP_DEAD_ZONE_DELTA) {
[view updateCursorLocation:touchLocation isMouse:NO];
}
// Press the left button down
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Start the long press timer
longPressTimer = [NSTimer scheduledTimerWithTimeInterval:LONG_PRESS_ACTIVATION_DELAY
target:self
selector:@selector(onLongPressStart:)
userInfo:nil
repeats:NO];
lastTouchDown = touch;
lastTouchDownLocation = touchLocation;
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
// Ignore touch move events with more than one finger
if ([[event allTouches] count] > 1) {
return;
}
UITouch* touch = [touches anyObject];
CGPoint touchLocation = [touch locationInView:view];
if (sqrt(pow((touchLocation.x / view.bounds.size.width) - (lastTouchDownLocation.x / view.bounds.size.width), 2) +
pow((touchLocation.y / view.bounds.size.height) - (lastTouchDownLocation.y / view.bounds.size.height), 2)) > LONG_PRESS_ACTIVATION_DELTA) {
// Moved too far since touch down. Cancel the long press timer.
[longPressTimer invalidate];
longPressTimer = nil;
}
[view updateCursorLocation:[[touches anyObject] locationInView:view] isMouse:NO];
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
// Only fire this logic if all touches have ended
if ([[event allTouches] count] == [touches count]) {
// Cancel the long press timer
[longPressTimer invalidate];
longPressTimer = nil;
// Left button up on finger up
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
// Raise right button too in case we triggered a long press gesture
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
// Remember this last touch for touch-down deadzoning
lastTouchUp = [touches anyObject];
lastTouchUpLocation = [lastTouchUp locationInView:view];
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
// Treat this as a normal touchesEnded event
[self touchesEnded:touches withEvent:event];
}
@end
+28 -2
View File
@@ -6,10 +6,18 @@
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
//
#import "HapticContext.h"
@import GameController;
@import CoreHaptics;
@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;
@@ -21,7 +29,25 @@
@property (nonatomic) short lastLeftStickY;
@property (nonatomic) short lastRightStickX;
@property (nonatomic) short lastRightStickY;
@property (nonatomic) unsigned short lowFreqMotor;
@property (nonatomic) unsigned short highFreqMotor;
@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
+8 -2
View File
@@ -11,15 +11,18 @@
@class OnScreenControls;
@protocol GamepadPresenceDelegate <NSObject>
@protocol ControllerSupportDelegate <NSObject>
- (void) gamepadPresenceChanged;
- (void) mousePresenceChanged;
- (void) streamExitRequested;
@end
@interface ControllerSupport : NSObject
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<GamepadPresenceDelegate>)delegate;
-(id) initWithConfig:(StreamConfiguration*)streamConfig delegate:(id<ControllerSupportDelegate>)delegate;
-(void) connectionEstablished;
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
-(void) cleanup;
@@ -39,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;
File diff suppressed because it is too large Load Diff
+22
View File
@@ -0,0 +1,22 @@
//
// HapticContext.h
// Moonlight
//
// Created by Cameron Gutman on 9/17/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
@import CoreHaptics;
@import GameController;
@interface HapticContext : NSObject
-(void)setMotorAmplitude:(unsigned short)amplitude;
-(void)cleanup;
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad;
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad;
+(HapticContext*) createContextForLeftTrigger:(GCController*)gamepad;
+(HapticContext*) createContextForRightTrigger:(GCController*)gamepad;
@end
+170
View File
@@ -0,0 +1,170 @@
//
// HapticContext.m
// Moonlight
//
// Created by Cameron Gutman on 9/17/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "HapticContext.h"
@import CoreHaptics;
@import GameController;
@implementation HapticContext {
GCControllerPlayerIndex _playerIndex;
CHHapticEngine* _hapticEngine API_AVAILABLE(ios(13.0), tvos(14.0));
id<CHHapticPatternPlayer> _hapticPlayer API_AVAILABLE(ios(13.0), tvos(14.0));
BOOL _playing;
}
-(void)cleanup API_AVAILABLE(ios(14.0), tvos(14.0)) {
if (_hapticPlayer != nil) {
[_hapticPlayer cancelAndReturnError:nil];
_hapticPlayer = nil;
}
if (_hapticEngine != nil) {
[_hapticEngine stopWithCompletionHandler:nil];
_hapticEngine = nil;
}
}
-(void)setMotorAmplitude:(unsigned short)amplitude API_AVAILABLE(ios(14.0), tvos(14.0)) {
NSError* error;
// Check if the haptic engine died
if (_hapticEngine == nil) {
return;
}
// Stop the effect entirely if the amplitude is 0
if (amplitude == 0) {
if (_playing) {
[_hapticPlayer stopAtTime:0 error:&error];
_playing = NO;
}
return;
}
if (_hapticPlayer == nil) {
// We must initialize the intensity to 1.0f because the dynamic parameters are multiplied by this value before being applied
CHHapticEventParameter* intensityParameter = [[CHHapticEventParameter alloc] initWithParameterID:CHHapticEventParameterIDHapticIntensity value:1.0f];
CHHapticEvent* hapticEvent = [[CHHapticEvent alloc] initWithEventType:CHHapticEventTypeHapticContinuous parameters:[NSArray arrayWithObject:intensityParameter] relativeTime:0 duration:GCHapticDurationInfinite];
CHHapticPattern* hapticPattern = [[CHHapticPattern alloc] initWithEvents:[NSArray arrayWithObject:hapticEvent] parameters:[[NSArray alloc] init] error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic pattern creation failed: %@", _playerIndex, error);
return;
}
_hapticPlayer = [_hapticEngine createPlayerWithPattern:hapticPattern error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic player creation failed: %@", _playerIndex, error);
return;
}
}
CHHapticDynamicParameter* intensityParameter = [[CHHapticDynamicParameter alloc] initWithParameterID:CHHapticDynamicParameterIDHapticIntensityControl value:amplitude / 65535.0f relativeTime:0];
[_hapticPlayer sendParameters:[NSArray arrayWithObject:intensityParameter] atTime:CHHapticTimeImmediate error:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic player parameter update failed: %@", _playerIndex, error);
return;
}
if (!_playing) {
[_hapticPlayer startAtTime:0 error:&error];
if (error != nil) {
_hapticPlayer = nil;
Log(LOG_W, @"Controller %d: Haptic playback start failed: %@", _playerIndex, error);
return;
}
_playing = YES;
}
}
-(id) initWithGamepad:(GCController*)gamepad locality:(GCHapticsLocality)locality API_AVAILABLE(ios(14.0), tvos(14.0)) {
if (gamepad.haptics == nil) {
Log(LOG_W, @"Controller %d does not support haptics", gamepad.playerIndex);
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];
NSError* error;
[_hapticEngine startAndReturnError:&error];
if (error != nil) {
Log(LOG_W, @"Controller %d: Haptic engine failed to start: %@", gamepad.playerIndex, error);
return nil;
}
__weak typeof(self) weakSelf = self;
_hapticEngine.stoppedHandler = ^(CHHapticEngineStoppedReason stoppedReason) {
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine stopped: %p", me->_playerIndex, stoppedReason);
me->_hapticPlayer = nil;
me->_hapticEngine = nil;
me->_playing = NO;
};
_hapticEngine.resetHandler = ^{
HapticContext* me = weakSelf;
if (me == nil) {
return;
}
Log(LOG_W, @"Controller %d: Haptic engine reset", me->_playerIndex);
me->_hapticPlayer = nil;
me->_playing = NO;
[me->_hapticEngine startAndReturnError:nil];
};
return self;
}
+(HapticContext*) createContextForHighFreqMotor:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityRightHandle];
}
else {
return nil;
}
}
+(HapticContext*) createContextForLowFreqMotor:(GCController*)gamepad {
if (@available(iOS 14.0, tvOS 14.0, *)) {
return [[HapticContext alloc] initWithGamepad:gamepad locality:GCHapticsLocalityLeftHandle];
}
else {
return nil;
}
}
+(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;
+56 -5
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;
@@ -21,9 +54,12 @@
if (key.modifierFlags & UIKeyModifierAlternate) {
modifierFlags |= MODIFIER_ALT;
}
if (key.modifierFlags & (UIKeyModifierControl | UIKeyModifierCommand)) {
if (key.modifierFlags & UIKeyModifierControl) {
modifierFlags |= MODIFIER_CTRL;
}
if (key.modifierFlags & UIKeyModifierCommand) {
modifierFlags |= MODIFIER_META;
}
// This converts UIKeyboardHIDUsage values to Win32 VK_* values
// https://docs.microsoft.com/en-us/windows/win32/inputdev/virtual-key-codes
@@ -165,7 +201,8 @@
keyCode = 0x6B;
break;
case UIKeyboardHIDUsageKeypadEnter:
keyCode = 0x0D; // FIXME: Is this correct?
keyCode = 0x0D;
modifierFlags |= MODIFIER_EXTENDED;
break;
case UIKeyboardHIDUsageKeypadPeriod:
keyCode = 0x6E;
@@ -189,6 +226,8 @@
keyCode = 0xF8;
break;
case UIKeyboardHIDUsageKeyboardLeftGUI:
keyCode = 0x5B;
break;
case UIKeyboardHIDUsageKeyboardLeftControl:
keyCode = 0xA2;
break;
@@ -199,6 +238,8 @@
keyCode = 0xA4;
break;
case UIKeyboardHIDUsageKeyboardRightGUI:
keyCode = 0x5C;
break;
case UIKeyboardHIDUsageKeyboardRightControl:
keyCode = 0xA3;
break;
@@ -208,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);
@@ -215,7 +259,7 @@
}
}
LiSendKeyboardEvent(keyCode,
LiSendKeyboardEvent(0x8000 | keyCode,
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
modifierFlags);
return true;
@@ -232,10 +276,12 @@
case UIKeyModifierShift:
[KeyboardSupport addShiftModifier:&event];
break;
case UIKeyModifierCommand:
case UIKeyModifierControl:
[KeyboardSupport addControlModifier:&event];
break;
case UIKeyModifierCommand:
[KeyboardSupport addMetaModifier:&event];
break;
case UIKeyModifierAlternate:
[KeyboardSupport addAltModifier:&event];
break;
@@ -393,6 +439,11 @@
event->modifierKeycode = 0x11;
}
+ (void) addMetaModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_META;
event->modifierKeycode = 0x5B;
}
+ (void) addAltModifier:(struct KeyEvent*)event {
event->modifier = MODIFIER_ALT;
event->modifierKeycode = 0x12;
+2 -2
View File
@@ -8,8 +8,8 @@
#import <Foundation/Foundation.h>
@protocol EdgeDetectionDelegate;
@class ControllerSupport;
@class StreamConfiguration;
@interface OnScreenControls : NSObject
@@ -25,7 +25,7 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
};
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig;
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
+37 -48
View File
@@ -36,7 +36,6 @@
CALayer* _l1Button;
CALayer* _l2Button;
CALayer* _l3Button;
CALayer* _edge;
UITouch* _aTouch;
UITouch* _bTouch;
@@ -53,7 +52,6 @@
UITouch* _l1Touch;
UITouch* _l2Touch;
UITouch* _l3Touch;
UITouch* _edgeTouch;
NSDate* l3TouchStart;
NSDate* r3TouchStart;
@@ -69,8 +67,8 @@
ControllerSupport *_controllerSupport;
Controller *_controller;
id<EdgeDetectionDelegate> _edgeDelegate;
NSMutableArray* _deadTouches;
BOOL _swapABXY;
}
static const float EDGE_WIDTH = .05;
@@ -114,13 +112,13 @@ static float L2_Y;
static float L3_X;
static float L3_Y;
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate {
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport streamConfig:(StreamConfiguration*)streamConfig {
self = [self init];
_view = view;
_controllerSupport = controllerSupport;
_controller = [controllerSupport getOscController];
_edgeDelegate = swipeDelegate;
_deadTouches = [[NSMutableArray alloc] init];
_swapABXY = streamConfig.swapABXYButtons;
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
@@ -156,16 +154,13 @@ static float L3_Y;
_rightStickBackground = [CALayer layer];
_leftStick = [CALayer layer];
_rightStick = [CALayer layer];
_edge = [CALayer layer];
return self;
}
- (void) show {
_visible = YES;
[self setupEdgeDetection];
[self updateControls];
}
@@ -254,11 +249,6 @@ static float L3_Y;
}
}
- (void) setupEdgeDetection {
_edge.frame = CGRectMake(0, 0, _view.frame.size.width * EDGE_WIDTH, _view.frame.size.height);
[_view.layer addSublayer:_edge];
}
// For GCExtendedGamepad controls we move start, select, L3, and R3 to the button
- (void) setupExtendedGamepadControls {
// Start with the default complex layout
@@ -384,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];
@@ -658,7 +654,7 @@ static float L3_Y;
if (updated) {
[_controllerSupport updateFinished:_controller];
}
return updated || buttonTouch;
return updated || buttonTouch;
}
- (BOOL)handleTouchDownEvent:touches {
@@ -667,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;
@@ -734,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;
@@ -745,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
@@ -757,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
@@ -769,8 +765,6 @@ static float L3_Y;
}
_rsTouch = touch;
stickTouch = true;
} else if ([_edge.presentationLayer hitTest:touchLocation]) {
_edgeTouch = touch;
}
if (!updated && !stickTouch && [self isInDeadZone:touch]) {
[_deadTouches addObject:touch];
@@ -854,11 +848,6 @@ static float L3_Y;
else if (touch == _r3Touch) {
_r3Touch = nil;
touched = true;
} else if (touch == _edgeTouch) {
_edgeTouch = nil;
if ([touch locationInView:_view].x >= _view.frame.size.width / 4) {
[_edgeDelegate edgeSwiped];
}
}
if ([_deadTouches containsObject:touch]) {
[_deadTouches removeObject:touch];
+19
View File
@@ -0,0 +1,19 @@
//
// RelativeTouchHandler.h
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "StreamView.h"
NS_ASSUME_NONNULL_BEGIN
@interface RelativeTouchHandler : UIResponder
-(id)initWithView:(StreamView*)view;
@end
NS_ASSUME_NONNULL_END
+207
View File
@@ -0,0 +1,207 @@
//
// RelativeTouchHandler.m
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
#import "RelativeTouchHandler.h"
#include <Limelight.h>
static const int REFERENCE_WIDTH = 1280;
static const int REFERENCE_HEIGHT = 720;
@implementation RelativeTouchHandler {
CGPoint touchLocation, originalLocation;
BOOL touchMoved;
BOOL isDragging;
NSTimer* dragTimer;
NSUInteger peakTouchCount;
#if TARGET_OS_TV
UIGestureRecognizer* remotePressRecognizer;
UIGestureRecognizer* remoteLongPressRecognizer;
#endif
UIView* view;
}
- (id)initWithView:(StreamView*)view {
self = [self init];
self->view = view;
#if TARGET_OS_TV
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
[self->view addGestureRecognizer:remotePressRecognizer];
[self->view addGestureRecognizer:remoteLongPressRecognizer];
#endif
return self;
}
- (BOOL)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
// Movements of greater than 5 pixels are considered confirmed
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 5;
}
- (void)onDragStart:(NSTimer*)timer {
if (!touchMoved && !isDragging){
isDragging = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
}
- (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];
if (!isDragging) {
dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650
target:self
selector:@selector(onDragStart:)
userInfo:nil
repeats:NO];
}
}
else if ([[event allTouches] count] == 2) {
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
originalLocation = touchLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
}
}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
if ([[event allTouches] count] == 1) {
UITouch *touch = [[event allTouches] anyObject];
CGPoint currentLocation = [touch locationInView:view];
if (touchLocation.x != currentLocation.x ||
touchLocation.y != currentLocation.y)
{
int deltaX = (currentLocation.x - touchLocation.x) * (REFERENCE_WIDTH / view.bounds.size.width);
int deltaY = (currentLocation.y - touchLocation.y) * (REFERENCE_HEIGHT / view.bounds.size.height);
if (deltaX != 0 || deltaY != 0) {
LiSendMouseMoveEvent(deltaX, deltaY);
touchLocation = currentLocation;
// If we've moved far enough to confirm this wasn't just human/machine error,
// mark it as such.
if ([self isConfirmedMove:touchLocation from:originalLocation]) {
touchMoved = true;
}
}
}
} else if ([[event allTouches] count] == 2) {
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:view];
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:view];
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
if (touchLocation.y != avgLocation.y) {
LiSendHighResScrollEvent((avgLocation.y - touchLocation.y) * 10);
}
// If we've moved far enough to confirm this wasn't just human/machine error,
// mark it as such.
if ([self isConfirmedMove:firstLocation from:originalLocation]) {
touchMoved = true;
}
touchLocation = avgLocation;
}
}
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
[dragTimer invalidate];
dragTimer = nil;
if (isDragging) {
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
} else if (!touchMoved) {
if (peakTouchCount == 2) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
Log(LOG_D, @"Sending right mouse button press");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
});
} 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");
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
}
self->isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
}
}
// We we're moving from 2+ touches to 1. Synchronize the current position
// of the active finger so we don't jump unexpectedly on the next touchesMoved
// callback when finger 1 switches on us.
if ([[event allTouches] count] - [touches count] == 1) {
NSMutableSet *activeSet = [[NSMutableSet alloc] initWithCapacity:[[event allTouches] count]];
[activeSet unionSet:[event allTouches]];
[activeSet minusSet:touches];
touchLocation = [[activeSet anyObject] locationInView:view];
// Mark this touch as moved so we don't send a left mouse click if the user
// right clicks without moving their other finger.
touchMoved = true;
}
}
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
[dragTimer invalidate];
dragTimer = nil;
if (isDragging) {
isDragging = false;
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
}
peakTouchCount = 0;
}
#if TARGET_OS_TV
- (void)remoteButtonPressed:(id)sender {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
Log(LOG_D, @"Sending left mouse button press");
// Mark this as touchMoved to avoid a duplicate press on touch up
self->touchMoved = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
// Wait 100 ms to simulate a real button press
usleep(100 * 1000);
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
});
}
- (void)remoteButtonLongPressed:(id)sender {
Log(LOG_D, @"Holding left mouse button");
isDragging = true;
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
}
#endif
@end
+7 -11
View File
@@ -8,14 +8,9 @@
#import "ControllerSupport.h"
#import "OnScreenControls.h"
#import "Moonlight-Swift.h"
#import "StreamConfiguration.h"
@protocol EdgeDetectionDelegate <NSObject>
- (void) edgeSwiped;
@end
@protocol UserInteractionDelegate <NSObject>
- (void) userInteractionBegan;
@@ -24,18 +19,19 @@
@end
#if TARGET_OS_TV
@interface StreamView : OSView <UITextFieldDelegate>
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
#else
@interface StreamView : OSView <UITextFieldDelegate, UIPointerInteractionDelegate>
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
#endif
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
config:(StreamConfiguration*)streamConfig;
- (void) showOnScreenControls;
- (OnScreenControlsLevel) getCurrentOscState;
#if !TARGET_OS_TV
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
#endif
@end
File diff suppressed because it is too large Load Diff
+12 -6
View File
@@ -4,6 +4,8 @@
<dict>
<key>CADisableMinimumFrameDuration</key>
<true/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
@@ -30,13 +32,13 @@
<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>
<string>public.app-category.games</string>
<key>LSRequiresIPhoneOS</key>
@@ -46,12 +48,16 @@
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
<key>NSBluetoothPeripheralUsageDescription</key>
<string>Bluetooth access allows Moonlight to connect to Citrix X1 mice.</string>
<key>NSBonjourServices</key>
<array>
<string>_nvstream._tcp</string>
</array>
<key>NSLocalNetworkUsageDescription</key>
<string>Moonlight uses the local network to connect to your gaming PC for streaming.</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
-1
View File
@@ -21,5 +21,4 @@
#import <Foundation/Foundation.h>
#import <CoreData/CoreData.h>
#import "Logger.h"
#include "OSPortabilityDefs.h"
#endif
@@ -3,6 +3,6 @@
<plist version="1.0">
<dict>
<key>_XCCurrentVersionName</key>
<string>Moonlight v1.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>
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19G2021" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17192" systemVersion="19H2" 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"/>
@@ -22,6 +23,7 @@
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="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"/>
@@ -29,13 +31,14 @@
<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="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
</entity>
<elements>
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
<element name="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="223"/>
<element name="Settings" positionX="0" positionY="0" width="128" height="268"/>
</elements>
</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>
+2 -2
View File
@@ -16,8 +16,8 @@
self.statusMessage = @"App asset has no status message";
self.statusCode = -1;
}
- (OSImage*) getImage {
return [[OSImage alloc] initWithData:self.data];
- (UIImage*) getImage {
return [[UIImage alloc] initWithData:self.data];
}
@end
+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);
-24
View File
@@ -1,24 +0,0 @@
//
// OSPortabilityDefs.h
// Moonlight
//
// Created by Cameron Gutman on 3/26/18.
// Copyright © 2018 Moonlight Stream. All rights reserved.
//
#ifndef OSPortabilityDefs_h
#define OSPortabilityDefs_h
#if TARGET_OS_IPHONE
#define OSImage UIImage
#define OSColor UIColor
#define OSView UIView
#define OSApplication UIApplication
#else
#define OSImage NSImage
#define OSColor NSColor
#define OSView NSView
#define OSApplication NSApplication
#endif
#endif /* OSPortabilityDefs_h */
+13 -12
View File
@@ -11,23 +11,24 @@
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
@protocol ConnectionCallbacks <NSObject>
- (void) connectionStarted;
- (void) connectionTerminated:(int)errorCode;
- (void) stageStarting:(const char*)stageName;
- (void) stageComplete:(const char*)stageName;
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
- (void) launchFailed:(NSString*)message;
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
- (void) connectionStatusUpdate:(int)status;
@end
typedef struct {
CFTimeInterval startTime;
CFTimeInterval endTime;
int totalFrames;
int receivedFrames;
int networkDroppedFrames;
int totalHostProcessingLatency;
int framesWithHostProcessingLatency;
int maxHostProcessingLatency;
int minHostProcessingLatency;
} video_stats_t;
@interface Connection : NSOperation <NSStreamDelegate>
-(id) initWithConfig:(StreamConfiguration*)config renderer:(VideoDecoderRenderer*)myRenderer connectionCallbacks:(id<ConnectionCallbacks>)callbacks;
-(void) terminate;
-(void) main;
-(BOOL) getVideoStats:(video_stats_t*)stats;
-(NSString*) getActiveCodecName;
@end
+246 -205
View File
@@ -9,10 +9,11 @@
#import "Connection.h"
#import "Utils.h"
#import <AudioUnit/AudioUnit.h>
#import <AVFoundation/AVFoundation.h>
#import <VideoToolbox/VideoToolbox.h>
#define SDL_MAIN_HANDLED
#import <SDL.h>
#include "Limelight.h"
#include "opus_multistream.h"
@@ -25,41 +26,107 @@
char _hostString[256];
char _appVersionString[32];
char _gfeVersionString[32];
char _rtspSessionUrl[128];
}
static NSLock* initLock;
static OpusMSDecoder* opusDecoder;
static id<ConnectionCallbacks> _callbacks;
static int lastFrameNumber;
static int activeVideoFormat;
static video_stats_t currentVideoStats;
static video_stats_t lastVideoStats;
static NSLock* videoStatsLock;
#define OUTPUT_BUS 0
static SDL_AudioDeviceID audioDevice;
static OPUS_MULTISTREAM_CONFIGURATION audioConfig;
static void* audioBuffer;
static int audioFrameSize;
// My iPod touch 5th Generation seems to really require 80 ms
// of buffering to deliver glitch-free playback :(
// FIXME: Maybe we can use a smaller buffer on more modern iOS versions?
#define CIRCULAR_BUFFER_DURATION 80
static int audioBufferEntries;
static int audioBufferWriteIndex;
static int audioBufferReadIndex;
static int audioBufferStride;
static int audioSamplesPerFrame;
static short* audioCircularBuffer;
#define AUDIO_QUEUE_BUFFERS 3
static AudioQueueRef audioQueue;
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
static VideoDecoderRenderer* renderer;
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
{
[renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate];
[renderer setupWithVideoFormat:videoFormat width:width height:height frameRate:redrawRate];
lastFrameNumber = 0;
activeVideoFormat = videoFormat;
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
return 0;
}
void DrCleanup(void)
void DrStart(void)
{
[renderer cleanup];
[renderer start];
}
void DrStop(void)
{
[renderer stop];
}
-(BOOL) getVideoStats:(video_stats_t*)stats
{
// We return lastVideoStats because it is a complete 1 second window
[videoStatsLock lock];
if (lastVideoStats.endTime != 0) {
memcpy(stats, &lastVideoStats, sizeof(*stats));
[videoStatsLock unlock];
return YES;
}
// No stats yet
[videoStatsLock unlock];
return NO;
}
-(NSString*) getActiveCodecName
{
switch (activeVideoFormat)
{
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:
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";
}
}
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
@@ -71,6 +138,46 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
// A frame was lost due to OOM condition
return DR_NEED_IDR;
}
CFTimeInterval now = CACurrentMediaTime();
if (!lastFrameNumber) {
currentVideoStats.startTime = now;
lastFrameNumber = decodeUnit->frameNumber;
}
else {
// Flip stats roughly every second
if (now - currentVideoStats.startTime >= 1.0f) {
currentVideoStats.endTime = now;
[videoStatsLock lock];
lastVideoStats = currentVideoStats;
[videoStatsLock unlock];
memset(&currentVideoStats, 0, sizeof(currentVideoStats));
currentVideoStats.startTime = now;
}
// Any frame number greater than m_LastFrameNumber + 1 represents a dropped frame
currentVideoStats.networkDroppedFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
currentVideoStats.totalFrames += decodeUnit->frameNumber - (lastFrameNumber + 1);
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++;
PLENTRY entry = decodeUnit->bufferList;
while (entry != NULL) {
@@ -79,7 +186,7 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
length:entry->length
bufferType:entry->bufferType
pts:decodeUnit->presentationTimeMs];
decodeUnit:decodeUnit];
if (ret != DR_OK) {
free(data);
return ret;
@@ -97,116 +204,60 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
return [renderer submitDecodeBuffer:data
length:offset
bufferType:BUFFER_TYPE_PICDATA
pts:decodeUnit->presentationTimeMs];
decodeUnit:decodeUnit];
}
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION originalOpusConfig, void* context, int flags)
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
{
int err;
AudioChannelLayout channelLayout = {};
OPUS_MULTISTREAM_CONFIGURATION opusConfig = *originalOpusConfig;
SDL_AudioSpec want, have;
// Initialize the circular buffer
audioBufferWriteIndex = audioBufferReadIndex = 0;
audioSamplesPerFrame = opusConfig.samplesPerFrame;
audioBufferStride = opusConfig.channelCount * opusConfig.samplesPerFrame;
audioBufferEntries = CIRCULAR_BUFFER_DURATION / (opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000));
audioCircularBuffer = malloc(audioBufferEntries * audioBufferStride * sizeof(short));
if (audioCircularBuffer == NULL) {
Log(LOG_E, @"Error allocating output queue\n");
if (SDL_InitSubSystem(SDL_INIT_AUDIO) < 0) {
Log(LOG_E, @"Failed to initialize audio subsystem: %s\n", SDL_GetError());
return -1;
}
SDL_zero(want);
want.freq = opusConfig->sampleRate;
want.format = AUDIO_S16;
want.channels = opusConfig->channelCount;
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());
ArCleanup();
return -1;
}
switch (opusConfig.channelCount) {
case 2:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Stereo;
break;
case 4:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_Quadraphonic;
break;
case 6:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_5_1;
break;
case 8:
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_AudioUnit_7_1;
// Swap SL/SR and RL/RR to match the selected channel layout
opusConfig.mapping[4] = originalOpusConfig->mapping[6];
opusConfig.mapping[5] = originalOpusConfig->mapping[7];
opusConfig.mapping[6] = originalOpusConfig->mapping[4];
opusConfig.mapping[7] = originalOpusConfig->mapping[5];
break;
default:
// Unsupported channel layout
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig.channelCount);
abort();
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,
opusConfig.streams,
opusConfig.coupledStreams,
opusConfig.mapping,
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
opusConfig->channelCount,
opusConfig->streams,
opusConfig->coupledStreams,
opusConfig->mapping,
&err);
// Configure the audio session for our app
NSError *audioSessionError = nil;
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
[audioSession setPreferredSampleRate:opusConfig.sampleRate error:&audioSessionError];
[audioSession setCategory:AVAudioSessionCategoryPlayback
withOptions:AVAudioSessionCategoryOptionMixWithOthers
error:&audioSessionError];
[audioSession setPreferredIOBufferDuration:(opusConfig.samplesPerFrame / (opusConfig.sampleRate / 1000)) / 1000.0
error:&audioSessionError];
[audioSession setActive: YES error: &audioSessionError];
// FIXME: Calling this breaks surround audio for some reason
//[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
OSStatus status;
AudioStreamBasicDescription audioFormat = {0};
audioFormat.mSampleRate = opusConfig.sampleRate;
audioFormat.mBitsPerChannel = 16;
audioFormat.mFormatID = kAudioFormatLinearPCM;
audioFormat.mFormatFlags = kAudioFormatFlagIsSignedInteger | kLinearPCMFormatFlagIsPacked;
audioFormat.mChannelsPerFrame = opusConfig.channelCount;
audioFormat.mBytesPerFrame = audioFormat.mChannelsPerFrame * (audioFormat.mBitsPerChannel / 8);
audioFormat.mBytesPerPacket = audioFormat.mBytesPerFrame;
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
audioFormat.mReserved = 0;
status = AudioQueueNewOutput(&audioFormat, FillOutputBuffer, nil, nil, nil, 0, &audioQueue);
if (status != noErr) {
Log(LOG_E, @"Error allocating output queue: %d\n", status);
return status;
if (opusDecoder == NULL) {
Log(LOG_E, @"Failed to create Opus decoder");
ArCleanup();
return -1;
}
// We need to specify a channel layout for surround sound configurations
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
if (status != noErr) {
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
return status;
}
// Start playback
SDL_PauseAudioDevice(audioDevice, 0);
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * opusConfig.samplesPerFrame, &audioBuffers[i]);
if (status != noErr) {
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
return status;
}
FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
}
// Disable lowering volume of other audio streams (SDL sets AVAudioSessionCategoryOptionDuckOthers by default)
[[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayback withOptions:AVAudioSessionCategoryOptionMixWithOthers error:nil];
status = AudioQueueStart(audioQueue, nil);
if (status != noErr) {
Log(LOG_E, @"Error starting queue: %d\n", status);
return status;
}
return status;
return 0;
}
void ArCleanup(void)
@@ -216,43 +267,43 @@ void ArCleanup(void)
opusDecoder = NULL;
}
// Stop before disposing to avoid massive delay inside
// AudioQueueDispose() (iOS bug?)
AudioQueueStop(audioQueue, true);
// Also frees buffers
AudioQueueDispose(audioQueue, true);
// Must be freed after the queue is stopped
if (audioCircularBuffer != NULL) {
free(audioCircularBuffer);
audioCircularBuffer = NULL;
if (audioDevice != 0) {
SDL_CloseAudioDevice(audioDevice);
audioDevice = 0;
}
// Audio session is now inactive
[[AVAudioSession sharedInstance] setActive: NO error: nil];
if (audioBuffer != NULL) {
SDL_free(audioBuffer);
audioBuffer = NULL;
}
SDL_QuitSubSystem(SDL_INIT_AUDIO);
}
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
{
int decodeLen;
// Check if there is space for this sample in the buffer. Again, this can race
// but in the worst case, we'll not see the sample callback having consumed a sample.
if (((audioBufferWriteIndex + 1) % audioBufferEntries) == audioBufferReadIndex) {
// Don't queue if there's already more than 30 ms of audio data waiting
// in Moonlight's audio queue.
if (LiGetPendingAudioDuration() > 30) {
return;
}
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
(short*)&audioCircularBuffer[audioBufferWriteIndex * audioBufferStride], audioSamplesPerFrame, 0);
(short*)audioBuffer, audioConfig.samplesPerFrame, 0);
if (decodeLen > 0) {
// Use a full memory barrier to ensure the circular buffer is written before incrementing the index
__sync_synchronize();
// Provide backpressure on the queue to ensure too many frames don't build up
// in SDL's audio queue.
while (SDL_GetQueuedAudioSize(audioDevice) / audioFrameSize > 10) {
SDL_Delay(1);
}
// This can race with the reader in the sample callback, however this is a benign
// race since we'll either read the original value of s_WriteIndex (which is safe,
// we just won't consider this sample) or the new value of s_WriteIndex
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % audioBufferEntries;
if (SDL_QueueAudio(audioDevice,
audioBuffer,
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
}
}
}
@@ -299,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
@@ -327,16 +399,26 @@ void ClConnectionStatusUpdate(int status)
initLock = [[NSLock alloc] init];
}
if (videoStatsLock == nil) {
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) - 1);
}
LiInitializeServerInformation(&_serverInfo);
@@ -345,6 +427,10 @@ void ClConnectionStatusUpdate(int status)
if (config.gfeVersion != nil) {
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
}
if (config.rtspSessionUrl != nil) {
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
}
_serverInfo.serverCodecModeSupport = config.serverCodecModeSupport;
renderer = myRenderer;
_callbacks = callbacks;
@@ -354,13 +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;
// 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
@@ -372,23 +458,6 @@ void ClConnectionStatusUpdate(int status)
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
_streamConfig.packetSize = 1392;
}
// HDR implies HEVC allowed
if (config.enableHdr) {
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 = config.allowHevc && 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);
@@ -397,20 +466,17 @@ 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.
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
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;
_arCallbacks.cleanup = ArCleanup;
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT |
CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
LiInitializeConnectionCallbacks(&_clCallbacks);
_clCallbacks.stageStarting = ClStageStarting;
@@ -421,39 +487,14 @@ 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;
}
static void FillOutputBuffer(void *aqData,
AudioQueueRef inAQ,
AudioQueueBufferRef inBuffer) {
inBuffer->mAudioDataByteSize = audioBufferStride * sizeof(short);
assert(inBuffer->mAudioDataByteSize == inBuffer->mAudioDataBytesCapacity);
// If the indexes aren't equal, we have a sample
if (audioBufferWriteIndex != audioBufferReadIndex) {
// Copy data to the audio buffer
memcpy(inBuffer->mAudioData,
&audioCircularBuffer[audioBufferReadIndex * audioBufferStride],
inBuffer->mAudioDataByteSize);
// Use a full memory barrier to ensure the circular buffer is read before incrementing the index
__sync_synchronize();
// This can race with the reader in the AudDecDecodeAndPlaySample function. This is
// not a problem because at worst, it just won't see that we've consumed this sample yet.
audioBufferReadIndex = (audioBufferReadIndex + 1) % audioBufferEntries;
}
else {
// No data, so play silence
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
}
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
}
-(void) main
{
[initLock lock];
+25
View File
@@ -0,0 +1,25 @@
//
// ConnectionCallbacks.h
// Moonlight
//
// Created by Cameron Gutman on 11/1/20.
// Copyright © 2020 Moonlight Game Streaming Project. All rights reserved.
//
@protocol ConnectionCallbacks <NSObject>
- (void) connectionStarted;
- (void) connectionTerminated:(int)errorCode;
- (void) stageStarting:(const char*)stageName;
- (void) stageComplete:(const char*)stageName;
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags;
- (void) launchFailed:(NSString*)message;
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
- (void) connectionStatusUpdate:(int)status;
- (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
+6 -3
View File
@@ -9,24 +9,27 @@
@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;
@property int bitRate;
@property int riKeyId;
@property BOOL streamingRemotely;
@property NSData* riKey;
@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, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioConfiguration, enableHdr, multiController, allowHevc, serverCert;
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport;
@end
+3 -1
View File
@@ -11,8 +11,10 @@
@interface StreamManager : NSOperation
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
- (void) stopStream;
- (NSString*) getStatsOverlayText;
@end
+78 -19
View File
@@ -17,15 +17,17 @@
#import "HttpRequest.h"
#import "IdManager.h"
#include <Limelight.h>
@implementation StreamManager {
StreamConfiguration* _config;
OSView* _renderView;
UIView* _renderView;
id<ConnectionCallbacks> _callbacks;
Connection* _connection;
}
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
self = [super init];
_config = config;
_renderView = view;
@@ -37,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];
@@ -65,15 +65,13 @@
return;
}
// resumeApp and launchApp handle calling launchFailed
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
// App already running, resume it
if (![self resumeApp:hMan]) {
return;
}
} else {
// Start app
if (![self launchApp:hMan]) {
// 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"];
if (codecSupport == nil || !([codecSupport intValue] & 0x200)) {
[_callbacks launchFailed:@"Your host PC's GPU doesn't support streaming video resolutions over 4K."];
return;
}
}
@@ -82,9 +80,26 @@
_config.appVersion = appversion;
_config.gfeVersion = gfeVersion;
// resumeApp and launchApp handle calling launchFailed
NSString* sessionUrl;
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
// App already running, resume it
if (![self resumeApp:hMan receiveSessionUrl:&sessionUrl]) {
return;
}
} else {
// Start app
if (![self launchApp:hMan receiveSessionUrl:&sessionUrl]) {
return;
}
}
// Populate RTSP session URL from launch/resume response
_config.rtspSessionUrl = sessionUrl;
// Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView];
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];
@@ -96,9 +111,9 @@
[_connection terminate];
}
- (BOOL) launchApp:(HttpManager*)hMan {
- (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];
@@ -110,12 +125,13 @@
return FALSE;
}
*sessionUrl = [launchResp getStringTag:@"sessionUrl0"];
return TRUE;
}
- (BOOL) resumeApp:(HttpManager*)hMan {
- (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];
@@ -127,7 +143,50 @@
return FALSE;
}
*sessionUrl = [resumeResp getStringTag:@"sessionUrl0"];
return TRUE;
}
- (NSString*) getStatsOverlayText {
video_stats_t stats;
if (!_connection) {
return nil;
}
if (![_connection getVideoStats:&stats]) {
return nil;
}
uint32_t rtt, variance;
NSString* latencyString;
if (LiGetEstimatedRttInfo(&rtt, &variance)) {
latencyString = [NSString stringWithFormat:@"%u ms (variance: %u ms)", rtt, variance];
}
else {
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: %@%@",
_config.width,
_config.height,
stats.totalFrames / interval,
[_connection getActiveCodecName],
stats.networkDroppedFrames / interval,
latencyString,
hostProcessingString];
}
@end
+10 -7
View File
@@ -8,16 +8,19 @@
@import AVFoundation;
#import "ConnectionCallbacks.h"
#include "Limelight.h"
@interface VideoDecoderRenderer : NSObject
- (id)initWithView:(OSView*)view;
- (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 pts:(unsigned int)pts;
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du;
@end
+563 -263
View File
@@ -9,19 +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
@@ -29,12 +42,23 @@
CALayer *oldLayer = displayLayer;
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
displayLayer.bounds = _view.bounds;
displayLayer.backgroundColor = [OSColor blackColor].CGColor;
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;
@@ -47,236 +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
- (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
{
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return !waitingForSps && !waitingForPps;
}
else {
// H.265 requires VPS in addition to SPS and PPS
return !waitingForVps && !waitingForSps && !waitingForPps;
}
}
- (Boolean)isNalReferencePicture:(unsigned char)nalType
{
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
return nalType == 0x65;
}
else {
// HEVC has several types of reference NALU types
switch (nalType) {
case 0x20:
case 0x22:
case 0x24:
case 0x26:
case 0x28:
case 0x2A:
return true;
default:
return false;
}
}
}
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
- (void)updateAnnexBBufferForRange:(CMBlockBufferRef)frameBuffer dataBlock:(CMBlockBufferRef)dataBuffer offset:(int)offset length:(int)nalLength
{
OSStatus status;
size_t oldOffset = CMBlockBufferGetDataLength(existingBuffer);
size_t oldOffset = CMBlockBufferGetDataLength(frameBuffer);
// 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;
}
// 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;
}
// 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;
}
}
- (NSData*)getAv1CodecConfigurationBox:(NSData*)frameData {
AVIOContext* ioctx = NULL;
int err;
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 pts:(unsigned int)pts
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType decodeUnit:(PDECODE_UNIT)du
{
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
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) {
@@ -289,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);
@@ -303,82 +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 (![self isNalReferencePicture:nalType]) {
// 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 ([self isNalReferencePicture:nalType]) {
// Ensure the layer is visible now
self->displayLayer.hidden = NO;
// 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
+6 -1
View File
@@ -11,11 +11,16 @@
@protocol AppCallback <NSObject>
- (void) appClicked:(TemporaryApp*) app;
- (void) appClicked:(TemporaryApp*)app view:(UIView*)view;
- (void) appLongClicked:(TemporaryApp*)app view:(UIView*)view;
@end
#if !TARGET_OS_TV
@interface UIAppView : UIButton <UIContextMenuInteractionDelegate>
#else
@interface UIAppView : UIButton
#endif
- (id) initWithApp:(TemporaryApp*)app cache:(NSCache*)cache andCallback:(id<AppCallback>)callback;
- (void) updateAppImage;
+59 -16
View File
@@ -9,7 +9,7 @@
#import "UIAppView.h"
#import "AppAssetManager.h"
static const float REFRESH_CYCLE = 2.0f;
static const float REFRESH_CYCLE = 1.0f;
@implementation UIAppView {
TemporaryApp* _app;
@@ -40,17 +40,28 @@ static UIImage* noImage;
self.frame = CGRectMake(0, 0, 150, 200);
#endif
[self setAlpha:app.hidden ? 0.4 : 1.0];
_appImage = [[UIImageView alloc] initWithFrame:self.frame];
[_appImage setImage:noImage];
[self addSubview:_appImage];
if (@available(iOS 9.0, tvOS 9.0, *)) {
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
// for tvOS devices and iOS prior to 13.0.
#if !TARGET_OS_TV
if (@available(iOS 13.0, *)) {
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:rightClickInteraction];
}
else {
[self addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
else
#endif
{
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(appLongClicked:)];
[self addGestureRecognizer:longPressRecognizer];
}
[self addTarget:self action:@selector(appClicked:) forControlEvents:UIControlEventPrimaryActionTriggered];
[self addTarget:self action:@selector(buttonSelected:) forControlEvents:UIControlEventTouchDown];
[self addTarget:self action:@selector(buttonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
@@ -69,15 +80,40 @@ static UIImage* noImage;
#endif
[self updateAppImage];
[self startUpdateLoop];
return self;
}
- (void) appClicked {
[_callback appClicked:_app];
- (void)didMoveToSuperview {
// Start our update loop when we are added to our cell
if (self.superview != nil) {
[self updateLoop];
}
}
- (void) appClicked:(UIView *)view {
[_callback appClicked:_app view:view];
}
- (void) appLongClicked:(UILongPressGestureRecognizer*)gesture {
if (gesture.state == UIGestureRecognizerStateBegan) {
[_callback appLongClicked:_app view:self];
}
}
#if !TARGET_OS_TV
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
configurationForMenuAtLocation:(CGPoint)location {
// We don't want to trigger the primary action at this point, so cancel
// tracking touch on this view now. This will also have the (intended)
// effect of removing the touch highlight on this view.
[self cancelTrackingWithEvent:nil];
[_callback appLongClicked:_app view:self];
return nil;
}
#endif
- (void) updateAppImage {
if (_appOverlay != nil) {
[_appOverlay removeFromSuperview];
@@ -174,21 +210,28 @@ static UIImage* noImage;
}
}
- (void) startUpdateLoop {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
- (void) updateLoop {
// Stop immediately if the view has been detached
if (self.superview == nil) {
return;
}
// Update the app image if neccessary
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
[self updateAppImage];
}
// Stop updating when we detach from our parent view
if (self.superview != nil) {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
// Show no shadow for hidden apps. Because we adjust the opacity of the
// cells for hidden apps, it makes them look bad when the shadow draws
// through the app tile.
self.superview.layer.shadowOpacity = _app.hidden ? 0.0f : 0.5f;
// Update opacity if neccessary
[self setAlpha:_app.hidden ? 0.4 : 1.0];
// Queue the next refresh cycle
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
@end
+4
View File
@@ -17,7 +17,11 @@
@end
#if !TARGET_OS_TV
@interface UIComputerView : UIButton <UIContextMenuInteractionDelegate>
#else
@interface UIComputerView : UIButton
#endif
- (id) initWithComputer:(TemporaryHost*)host andCallback:(id<HostCallback>)callback;
- (id) initForAddWithCallback:(id<HostCallback>)callback;
+42 -23
View File
@@ -102,12 +102,7 @@ static const int LABEL_DY = 20;
self = [self init];
_callback = callback;
if (@available(iOS 9.0, tvOS 9.0, *)) {
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
[_hostLabel setText:@"Add Host Manually"];
[_hostLabel sizeToFit];
@@ -124,22 +119,34 @@ static const int LABEL_DY = 20;
_host = host;
_callback = callback;
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
[self addGestureRecognizer:longPressRecognizer];
// Use UIContextMenuInteraction on iOS 13.0+ and a standard UILongPressGestureRecognizer
// for tvOS devices and iOS prior to 13.0.
#if !TARGET_OS_TV
if (@available(iOS 13.0, *)) {
UIContextMenuInteraction* rightClickInteraction = [[UIContextMenuInteraction alloc] initWithDelegate:self];
[self addInteraction:rightClickInteraction];
}
else
#endif
{
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
[self addGestureRecognizer:longPressRecognizer];
}
if (@available(iOS 9.0, tvOS 9.0, *)) {
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
}
else {
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
}
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
[self updateContentsForHost:host];
[self startUpdateLoop];
return self;
}
- (void)didMoveToSuperview {
// Start our update loop when we are added to our cell
if (self.superview != nil && _host != nil) {
[self updateLoop];
}
}
- (void) updateBounds {
float x = FLT_MAX;
float y = FLT_MAX;
@@ -190,17 +197,16 @@ static const int LABEL_DY = 20;
[self updateBounds];
}
- (void) startUpdateLoop {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
- (void) updateLoop {
// Stop immediately if the view has been detached
if (self.superview == nil) {
return;
}
[self updateContentsForHost:_host];
// Stop updating when we detach from our parent view
if (self.superview != nil) {
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
// Queue the next refresh cycle
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
}
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
@@ -209,6 +215,19 @@ static const int LABEL_DY = 20;
}
}
#if !TARGET_OS_TV
- (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction
configurationForMenuAtLocation:(CGPoint)location {
// We don't want to trigger the primary action at this point, so cancel
// tracking touch on this view now. This will also have the (intended)
// effect of removing the touch highlight on this view.
[self cancelTrackingWithEvent:nil];
[_callback hostLongClicked:_host view:self];
return nil;
}
#endif
- (void) hostClicked {
[_callback hostClicked:_host view:self];
}
+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)
@@ -40,6 +40,7 @@
@implementation MainFrameViewController {
NSOperationQueue* _opQueue;
TemporaryHost* _selectedHost;
BOOL _showHiddenApps;
NSString* _uniqueId;
NSData* _clientCert;
DiscoveryManager* _discMan;
@@ -63,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;
@@ -167,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];
@@ -208,6 +209,24 @@ static NSMutableSet* hostList;
});
}
- (void) updateAppEntry:(TemporaryApp*)app forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init];
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
for (TemporaryApp* savedApp in newHostAppList) {
if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name;
savedApp.hdrSupported = app.hdrSupported;
savedApp.hidden = app.hidden;
host.appList = newHostAppList;
[database updateAppsForExistingHost:host];
return;
}
}
}
- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
DataManager* database = [[DataManager alloc] init];
NSMutableSet* newHostAppList = [NSMutableSet setWithSet:host.appList];
@@ -218,6 +237,7 @@ static NSMutableSet* hostList;
if ([app.id isEqualToString:savedApp.id]) {
savedApp.name = app.name;
savedApp.hdrSupported = app.hdrSupported;
// Don't propagate hidden, because we want the local data to prevail
appAlreadyInList = YES;
break;
}
@@ -275,6 +295,7 @@ static NSMutableSet* hostList;
#endif
[_appManager stopRetrieving];
_showHiddenApps = NO;
_selectedHost = nil;
_sortedAppList = nil;
@@ -352,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
@@ -466,6 +487,20 @@ static NSMutableSet* hostList;
[[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
}]];
}
else if (host.pairState == PairStatePaired) {
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"View All Apps" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
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:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
@@ -482,16 +517,9 @@ static NSMutableSet* hostList;
message = @"The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later.";
}
else {
message = @"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n";
for (int i = 0; i < 32; i++) {
if (portTestResult & (1 << i)) {
NSString* portString = [NSString stringWithFormat:@"%s %u\n",
LiGetProtocolFromPortFlagIndex(i) == 17 ? "UDP" : "TCP",
LiGetPortFromPortFlagIndex(i)];
message = [message stringByAppendingString:portString];
}
}
char blockedPorts[512];
LiStringifyPortFlags(portTestResult, "\n", blockedPorts, sizeof(blockedPorts));
message = [NSString stringWithFormat:@"Your current network connection seems to be blocking Moonlight. Streaming may not work while connected to this network.\n\nThe following network ports were blocked:\n%s", blockedPorts];
}
UIAlertController* netTestAlert = [UIAlertController alertControllerWithTitle:@"Network Test Complete" message:message preferredStyle:UIAlertControllerStyleAlert];
@@ -504,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
@@ -525,9 +556,7 @@ static NSMutableSet* hostList;
longClickAlert.popoverPresentationController.sourceView = view;
longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
[[self activeViewController] presentViewController:longClickAlert animated:YES completion:^{
[self updateHosts];
}];
[[self activeViewController] presentViewController:longClickAlert animated:YES completion:nil];
}
- (void) addHostClicked {
@@ -535,7 +564,7 @@ static NSMutableSet* hostList;
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:@"If Moonlight doesn't find your local gaming PC automatically,\nenter the IP address of your PC" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
NSString* hostAddress = [((UITextField*)[[alertController textFields] objectAtIndex:0]).text trim];
[self showLoadingFrame:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
@@ -575,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;
@@ -605,46 +635,75 @@ static NSMutableSet* hostList;
#endif
_streamConfig.bitRate = [streamSettings.bitrate intValue];
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
_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) appClicked:(TemporaryApp *)app {
Log(LOG_D, @"Clicked app: %@", app.name);
- (void)appLongClicked:(TemporaryApp *)app view:(UIView *)view {
Log(LOG_D, @"Long clicked app: %@", app.name);
[_appManager stopRetrieving];
@@ -656,24 +715,49 @@ static NSMutableSet* hostList;
[[self revealViewController] revealToggleAnimated:NO];
}
#endif
TemporaryApp* currentApp = [self findRunningApp:app.host];
NSString* message;
if (currentApp == nil || [app.id isEqualToString:currentApp.id]) {
if (app.hidden) {
message = @"Hidden";
}
else {
message = @"";
}
}
else {
message = [NSString stringWithFormat:@"%@ is currently running", currentApp.name];
}
UIAlertController* alertController = [UIAlertController
alertControllerWithTitle: app.name
message:message
preferredStyle:UIAlertControllerStyleActionSheet];
[alertController addAction:[UIAlertAction
actionWithTitle:currentApp == nil ? @"Launch App" : ([app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App") style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
if (currentApp != nil) {
Log(LOG_I, @"Resuming application: %@", currentApp.name);
[self prepareToStreamApp:currentApp];
}
else {
Log(LOG_I, @"Launching application: %@", app.name);
[self prepareToStreamApp:app];
}
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}]];
if (currentApp != nil) {
UIAlertController* alertController = [UIAlertController
alertControllerWithTitle: app.name
message: [app.id isEqualToString:currentApp.id] ? @"" : [NSString stringWithFormat:@"%@ is currently running", currentApp.name]preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction
actionWithTitle:[app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
Log(LOG_I, @"Resuming application: %@", currentApp.name);
[self prepareToStreamApp:currentApp];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
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]];
@@ -714,9 +798,6 @@ static NSMutableSet* hostList;
else {
app.host.currentGame = @"0";
dispatch_async(dispatch_get_main_queue(), ^{
// Refresh the UI
[self updateAppsForHost:app.host];
// If it succeeds and we're to start streaming, segue to the stream
if (![app.id isEqualToString:currentApp.id]) {
[self prepareToStreamApp:app];
@@ -734,8 +815,46 @@ static NSMutableSet* hostList;
}];
}]];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
}
if (currentApp == nil || ![app.id isEqualToString:currentApp.id] || app.hidden) {
[alertController addAction:[UIAlertAction actionWithTitle:app.hidden ? @"Show App" : @"Hide App"
style:app.hidden ? UIAlertActionStyleDefault : UIAlertActionStyleDestructive
handler:^(UIAlertAction* action) {
app.hidden = !app.hidden;
[self updateAppEntry:app forHost:app.host];
// Don't call updateAppsForHost because that will nuke this
// app immediately if we're not showing hidden apps.
}]];
}
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
// these two lines are required for iPad support of UIAlertSheet
alertController.popoverPresentationController.sourceView = view;
alertController.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
}
- (void) appClicked:(TemporaryApp *)app view:(UIView *)view {
Log(LOG_D, @"Clicked app: %@", app.name);
[_appManager stopRetrieving];
#if !TARGET_OS_TV
if (currentPosition != FrontViewPositionLeft) {
// This must not be animated because we need the position
// to change (and notify our callback to save settings data)
// before we call prepareToStreamApp.
[[self revealViewController] revealToggleAnimated:NO];
}
#endif
if ([self findRunningApp:app.host]) {
// If there's a running app, display a menu
[self appLongClicked:app view:view];
} else {
[self prepareToStreamApp:app];
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
@@ -764,7 +883,7 @@ static NSMutableSet* hostList;
#if TARGET_OS_TV
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
[self appClicked:_sortedAppList[indexPath.row]];
[self appClicked:_sortedAppList[indexPath.row] view:nil];
}
#endif
@@ -868,6 +987,11 @@ static NSMutableSet* hostList;
self.collectionView.allowsMultipleSelection = NO;
#if !TARGET_OS_TV
self.collectionView.multipleTouchEnabled = NO;
#else
// This is the only way to get long press events on a UICollectionViewCell :(
UILongPressGestureRecognizer* cellLongPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleCollectionViewLongPress:)];
cellLongPress.delaysTouchesBegan = YES;
[self.collectionView addGestureRecognizer:cellLongPress];
#endif
[self retrieveSavedHosts];
@@ -883,6 +1007,21 @@ static NSMutableSet* hostList;
}
#if TARGET_OS_TV
-(void)handleCollectionViewLongPress:(UILongPressGestureRecognizer *)gestureRecognizer
{
// FIXME: Something is delaying touches so we only get to the Begin state
// before we actually want to signal the long press.
if (gestureRecognizer.state != UIGestureRecognizerStateBegan) {
return;
}
CGPoint point = [gestureRecognizer locationInView:self.collectionView];
NSIndexPath *indexPath = [self.collectionView indexPathForItemAtPoint:point];
if (indexPath != nil) {
[self appLongClicked:_sortedAppList[indexPath.row] view:nil];
}
}
- (void)openTvSettings:(id)sender
{
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
@@ -1041,14 +1180,17 @@ static NSMutableSet* hostList;
}
- (void) updateAllHosts:(NSArray *)hosts {
// We must copy the array here because it could be modified
// before our main thread dispatch happens.
NSArray* hostsCopy = [NSArray arrayWithArray:hosts];
dispatch_async(dispatch_get_main_queue(), ^{
Log(LOG_D, @"New host list:");
for (TemporaryHost* host in hosts) {
for (TemporaryHost* host in hostsCopy) {
Log(LOG_D, @"Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t ipv6Address:%@ \n\t uuid:%@ \n\t mac:%@ \n\t pairState:%d \n\t online:%d \n\t activeAddress:%@ \n}", host.name, host.address, host.localAddress, host.externalAddress, host.ipv6Address, host.uuid, host.mac, host.pairState, host.state, host.activeAddress);
}
@synchronized(hostList) {
[hostList removeAllObjects];
[hostList addObjectsFromArray:hosts];
[hostList addObjectsFromArray:hostsCopy];
}
[self updateHosts];
});
@@ -1056,27 +1198,25 @@ static NSMutableSet* hostList;
- (void)updateHostShortcuts {
#if !TARGET_OS_TV
if (@available(iOS 9.0, *)) {
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
@synchronized (hostList) {
for (TemporaryHost* host in hostList) {
// Pair state may be unknown if we haven't polled it yet, but the app list
// count will persist from paired PCs
if ([host.appList count] > 0) {
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
initWithType:@"PC"
localizedTitle:host.name
localizedSubtitle:nil
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
[quickActions addObject: shortcut];
}
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
@synchronized (hostList) {
for (TemporaryHost* host in hostList) {
// Pair state may be unknown if we haven't polled it yet, but the app list
// count will persist from paired PCs
if ([host.appList count] > 0) {
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
initWithType:@"PC"
localizedTitle:host.name
localizedSubtitle:nil
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
[quickActions addObject: shortcut];
}
}
[UIApplication sharedApplication].shortcutItems = quickActions;
}
[UIApplication sharedApplication].shortcutItems = quickActions;
#endif
}
@@ -1189,6 +1329,16 @@ static NSMutableSet* hostList;
_sortedAppList = [host.appList allObjects];
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
if (!_showHiddenApps) {
NSMutableArray* visibleAppList = [NSMutableArray array];
for (TemporaryApp* app in _sortedAppList) {
if (!app.hidden) {
[visibleAppList addObject:app];
}
}
_sortedAppList = visibleAppList;
}
[hostScrollView removeFromSuperview];
[self.collectionView reloadData];
}
@@ -1207,13 +1357,13 @@ static NSMutableSet* hostList;
[cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added
[cell addSubview:appView];
// Shadow opacity is controlled inside UIAppView based on whether the app
// is hidden or not during the update cycle.
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds];
cell.layer.masksToBounds = NO;
cell.layer.shadowColor = [UIColor blackColor].CGColor;
cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f);
cell.layer.shadowOpacity = 0.5f;
cell.layer.shadowPath = shadowPath.CGPath;
#if !TARGET_OS_TV
@@ -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;
@@ -1387,15 +1387,11 @@ const int FrontViewPositionNone = 0xff;
// These are derived from the primary view controller
if (@available(iOS 11.0, *)) {
// iOS 11 betas (which are still installed in some places :\) crash with
// doesNotRecognizeSelector since this wasn't added until after the iPhone X
// announcement.
if ([self respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
if ([self respondsToSelector: @selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
}
if (@available(iOS 14.0, *)) {
[self setNeedsUpdateOfPrefersPointerLocked];
}
}
@@ -1409,6 +1405,11 @@ const int FrontViewPositionNone = 0xff;
return _primaryViewController;
}
- (UIViewController*)childViewControllerForPointerLock
{
return _primaryViewController;
}
#pragma mark Animated view controller deployment and layout
// Primitive method for view controller deployment and animated layout to the given position.

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