mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2026-10-13 20:14:01 +00:00
Compare commits
284
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
512c1479a0 | ||
|
|
8391c766c7 | ||
|
|
eccf517dc3 | ||
|
|
90d416ab34 | ||
|
|
283a5516d8 | ||
|
|
dee18fb5c2 | ||
|
|
c99ee24c65 | ||
|
|
76ef2ed432 | ||
|
|
92a2bbe28e | ||
|
|
3dd0e8362a | ||
|
|
cea8ed485f | ||
|
|
505d248472 | ||
|
|
3722de4011 | ||
|
|
3f530afca4 | ||
|
|
d7cf8ced25 | ||
|
|
c930a4656a | ||
|
|
e80a4fd2b1 | ||
|
|
68e6690e55 | ||
|
|
6c45a5c43a | ||
|
|
6490bb6d67 | ||
|
|
93bcd8615c | ||
|
|
0fe84a7a1e | ||
|
|
7ed6b22cac | ||
|
|
295cf61727 | ||
|
|
c234c82e63 | ||
|
|
e143dff76a | ||
|
|
d317cdc476 | ||
|
|
b29259a330 | ||
|
|
f804d000f9 | ||
|
|
9baaa10417 | ||
|
|
073873f681 | ||
|
|
ab5a0a4470 | ||
|
|
fd1db224cb | ||
|
|
2d8d176160 | ||
|
|
a4035c18b5 | ||
|
|
05aedd5510 | ||
|
|
fd5426b7e8 | ||
|
|
651559727f | ||
|
|
76751dbee2 | ||
|
|
1c3d072b49 | ||
|
|
d88321263f | ||
|
|
867777468f | ||
|
|
d52703c20e | ||
|
|
3e90e2ebf2 | ||
|
|
16acd71f6a | ||
|
|
4079af5d22 | ||
|
|
97f96bb116 | ||
|
|
81b9a3e859 | ||
|
|
5da55d88c5 | ||
|
|
1fdbd84cd4 | ||
|
|
a66d6eb11b | ||
|
|
d92437cfc9 | ||
|
|
f719e080f9 | ||
|
|
b7740eaffd | ||
|
|
7b89d3813b | ||
|
|
b51cd67327 | ||
|
|
6e480a46a0 | ||
|
|
a3fe8e23f1 | ||
|
|
e76344812e | ||
|
|
eed2b82230 | ||
|
|
ecd3427767 | ||
|
|
311bf33e84 | ||
|
|
ea96bac31a | ||
|
|
f09b2fd222 | ||
|
|
8b8bc2f818 | ||
|
|
949f088ef5 | ||
|
|
e29313d9b6 | ||
|
|
9e669d13fa | ||
|
|
015cfecdd6 | ||
|
|
c623d40600 | ||
|
|
65293dab30 | ||
|
|
f3635a515c | ||
|
|
6b823e155a | ||
|
|
03f330ccff | ||
|
|
c301891ff5 | ||
|
|
1ba1ad64ec | ||
|
|
1f24d28ddd | ||
|
|
e9e03c74e2 | ||
|
|
0c6c3ccc83 | ||
|
|
07b8e7fd50 | ||
|
|
60a6582380 | ||
|
|
f5e0443abb | ||
|
|
9b8a179f4a | ||
|
|
78f2b8ac47 | ||
|
|
dc9b5b7c96 | ||
|
|
313af5a7e7 | ||
|
|
bb885465f6 | ||
|
|
e258551008 | ||
|
|
e402902d6e | ||
|
|
f55daf941c | ||
|
|
a483c6ea41 | ||
|
|
3ad4d857e8 | ||
|
|
6b1d34e4a9 | ||
|
|
bf36eaf661 | ||
|
|
e809afdd9e | ||
|
|
0d75dd4efb | ||
|
|
b799978cac | ||
|
|
42f29c44e6 | ||
|
|
c3ba447372 | ||
|
|
a358cdad3d | ||
|
|
a1f09f117f | ||
|
|
7270554153 | ||
|
|
8d127decb6 | ||
|
|
8ffee9e10f | ||
|
|
0eac28a74f | ||
|
|
5d94800e11 | ||
|
|
44f713f5c9 | ||
|
|
2114e39237 | ||
|
|
6fd8baee41 | ||
|
|
63beaebe55 | ||
|
|
17d4079a5a | ||
|
|
b650119fe9 | ||
|
|
8b28606952 | ||
|
|
24258d6e17 | ||
|
|
3f1699258d | ||
|
|
34f8696a5e | ||
|
|
96e317fe31 | ||
|
|
ecad1bea38 | ||
|
|
e4534fd4af | ||
|
|
126735fa57 | ||
|
|
9ff5fcb9d5 | ||
|
|
8c66898765 | ||
|
|
5faf1faf32 | ||
|
|
7e704ee201 | ||
|
|
c3ae83f902 | ||
|
|
95e16c5e2d | ||
|
|
fc08a7ee0e | ||
|
|
95e12f9853 | ||
|
|
88c18ad397 | ||
|
|
5043fadace | ||
|
|
6fa5ef73f6 | ||
|
|
c0367f711b | ||
|
|
0a5499f369 | ||
|
|
26209f187e | ||
|
|
e8c7eb67c6 | ||
|
|
2d24b0ec7b | ||
|
|
dfec190b82 | ||
|
|
88f9423802 | ||
|
|
beeec54420 | ||
|
|
b7b7a88ef7 | ||
|
|
6bf643968b | ||
|
|
dbab07838d | ||
|
|
06a4ced692 | ||
|
|
fb9d778dab | ||
|
|
2a0b29a284 | ||
|
|
bf437669fb | ||
|
|
e445989ec7 | ||
|
|
0b712a1589 | ||
|
|
1aa11df725 | ||
|
|
ff5e8d167d | ||
|
|
286b19d360 | ||
|
|
3ab0829db5 | ||
|
|
6f2041dfb6 | ||
|
|
78600a4e06 | ||
|
|
e0bb48320f | ||
|
|
46b9f84a31 | ||
|
|
abb4b5f9b9 | ||
|
|
a4f7861ccf | ||
|
|
39c03bc8a8 | ||
|
|
059a943a3d | ||
|
|
3e2e4d13a9 | ||
|
|
4a537c1806 | ||
|
|
839f2018cb | ||
|
|
f9d1840941 | ||
|
|
7747188363 | ||
|
|
c43ed56751 | ||
|
|
a3b01b3cd4 | ||
|
|
96c2ac9be5 | ||
|
|
aee4e3bf3d | ||
|
|
d9cb55a17f | ||
|
|
4c8ad13f3a | ||
|
|
99c160f8e5 | ||
|
|
78e16b0e29 | ||
|
|
fdc9e00c54 | ||
|
|
b16eaafd2c | ||
|
|
2ec6639351 | ||
|
|
f49a908dc0 | ||
|
|
b3a666af8e | ||
|
|
5abad38956 | ||
|
|
7f034e0338 | ||
|
|
fb196f661c | ||
|
|
3cca55703e | ||
|
|
084018719a | ||
|
|
4aca666df4 | ||
|
|
b833d3b3b7 | ||
|
|
cb94d7aad7 | ||
|
|
b8631db5df | ||
|
|
bd5e0ecc40 | ||
|
|
10d2e1635b | ||
|
|
68e59aba74 | ||
|
|
a4255f4cad | ||
|
|
f12e2f16dc | ||
|
|
177ecdbe0b | ||
|
|
de05214728 | ||
|
|
b0149b2fe9 | ||
|
|
1366ede690 | ||
|
|
fdaf7f92a1 | ||
|
|
8efbe2670a | ||
|
|
8bc66749c3 | ||
|
|
4ab71a3cc7 | ||
|
|
55b2f39721 | ||
|
|
e59f0764a2 | ||
|
|
6194d80108 | ||
|
|
5264fee655 | ||
|
|
ab17c57c09 | ||
|
|
c7a7fc3fbc | ||
|
|
f32d4f2886 | ||
|
|
8e4471fbc0 | ||
|
|
733b992912 | ||
|
|
6d03f4bc4c | ||
|
|
75ab28a2c3 | ||
|
|
69d1ff15a1 | ||
|
|
eceb23e1cd | ||
|
|
ab5a9ba067 | ||
|
|
742bb7b516 | ||
|
|
65578b8a64 | ||
|
|
1246de3f18 | ||
|
|
5b5fdff6d8 | ||
|
|
47aaa02d08 | ||
|
|
b71f06aa04 | ||
|
|
61c18c201c | ||
|
|
32b7396070 | ||
|
|
c327c5b1ec | ||
|
|
44d4553031 | ||
|
|
a2b8e3a8b8 | ||
|
|
9e91627601 | ||
|
|
250f6618fd | ||
|
|
ab5e253a59 | ||
|
|
12a64f8626 | ||
|
|
99fec8919e | ||
|
|
52dcdd68c4 | ||
|
|
5a0e4bcc05 | ||
|
|
2577f581ba | ||
|
|
1757359fbf | ||
|
|
e89707e601 | ||
|
|
c89de92e23 | ||
|
|
ee7df54842 | ||
|
|
95ce78bb00 | ||
|
|
3c9314dff3 | ||
|
|
c5e1bbf61b | ||
|
|
abe850cd72 | ||
|
|
b0a3e163c4 | ||
|
|
2ae7fefc0c | ||
|
|
72af5d5217 | ||
|
|
d1eb7b45a2 | ||
|
|
3b5652c3f5 | ||
|
|
df50bb9583 | ||
|
|
18e2d67f6b | ||
|
|
9f19f5da27 | ||
|
|
005c3e4082 | ||
|
|
35b3c01582 | ||
|
|
d8f55b57fa | ||
|
|
5faa8a0b85 | ||
|
|
967ddd7d68 | ||
|
|
26fac5b56e | ||
|
|
75c1c85de9 | ||
|
|
653a72eaf8 | ||
|
|
27d95ae0ba | ||
|
|
7dca4c6c63 | ||
|
|
12e7dd2181 | ||
|
|
c9225d468a | ||
|
|
a824c23c59 | ||
|
|
a9e01fc9a9 | ||
|
|
fb2d58da6a | ||
|
|
09ac6cc057 | ||
|
|
1121a918d1 | ||
|
|
565a8e56e6 | ||
|
|
f8cc7bb77f | ||
|
|
55c52e1ee1 | ||
|
|
366a94d018 | ||
|
|
4e5264d5e9 | ||
|
|
d98aba66a8 | ||
|
|
8ccbdc7923 | ||
|
|
769840d089 | ||
|
|
33b050f84f | ||
|
|
d2bc7209f7 | ||
|
|
d32d1aa09a | ||
|
|
7f6db298d0 | ||
|
|
f9ff6a6004 | ||
|
|
a6e6d0e491 | ||
|
|
6613976291 | ||
|
|
6f8d898b77 | ||
|
|
d1cbfd9c44 | ||
|
|
e6b1dd6b64 |
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
blank_issues_enabled: false
|
||||
@@ -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.
|
||||
@@ -0,0 +1,4 @@
|
||||
issuesOpened: >
|
||||
If this is a question about Moonlight or you need help troubleshooting a streaming problem, please use the help channels on our [Discord server](https://moonlight-stream.org/discord) instead of GitHub issues. There are many more people available on Discord to help you and answer your questions.<br /><br />
|
||||
This issue tracker should only be used for specific bugs or feature requests.<br /><br />
|
||||
Thank you, and happy streaming!
|
||||
@@ -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
|
||||
@@ -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
@@ -1,25 +0,0 @@
|
||||
language: objective-c
|
||||
|
||||
git:
|
||||
depth: 1
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- os: osx
|
||||
osx_image: xcode11
|
||||
env:
|
||||
- SCHEME="Moonlight"
|
||||
- TARGET="platform=iOS Simulator,OS=12.1,name=iPhone SE"
|
||||
- os: osx
|
||||
osx_image: xcode11
|
||||
env:
|
||||
- SCHEME="Moonlight"
|
||||
- TARGET="platform=iOS Simulator,OS=12.1,name=iPad Pro (9.7-inch)"
|
||||
- os: osx
|
||||
osx_image: xcode11
|
||||
env:
|
||||
- SCHEME="Moonlight TV"
|
||||
- TARGET="platform=tvOS Simulator,OS=12.1,name=Apple TV 4K"
|
||||
|
||||
script:
|
||||
- set -o pipefail && xcodebuild -project Moonlight.xcodeproj -scheme "$SCHEME" -destination "$TARGET" build | xcpretty
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+8
-24
@@ -1,35 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="14868" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="15505" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="15509"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<placeholder placeholderIdentifier="IBFilesOwner" id="-1" userLabel="File's Owner"/>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="-2" customClass="UIResponder"/>
|
||||
<navigationController id="vV9-32-ww6">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="y2P-aE-Haa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="896" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<color key="barTintColor" red="0.66666668653488159" green="0.66666668653488159" blue="0.66666668653488159" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</navigationBar>
|
||||
<viewControllers>
|
||||
<viewController id="l6V-ue-5Om">
|
||||
<layoutGuides>
|
||||
<viewControllerLayoutGuide type="top" id="rEZ-vx-akr"/>
|
||||
<viewControllerLayoutGuide type="bottom" id="gBD-cy-cth"/>
|
||||
</layoutGuides>
|
||||
<view key="view" contentMode="scaleToFill" id="6lF-fc-kpK">
|
||||
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<color key="backgroundColor" red="0.3333333432674408" green="0.3333333432674408" blue="0.3333333432674408" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</view>
|
||||
<navigationItem key="navigationItem" id="rW0-BH-s7l"/>
|
||||
</viewController>
|
||||
</viewControllers>
|
||||
<point key="canvasLocation" x="34" y="149"/>
|
||||
</navigationController>
|
||||
<view contentMode="scaleToFill" id="VWy-YH-qYj">
|
||||
<rect key="frame" x="0.0" y="0.0" width="896" height="414"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<color key="backgroundColor" white="0.33333333329999998" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
<point key="canvasLocation" x="33" y="187"/>
|
||||
</view>
|
||||
</objects>
|
||||
</document>
|
||||
|
||||
@@ -6,19 +6,13 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
#else
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
@property (readonly, strong) NSPersistentContainer *persistentContainer;
|
||||
@property (strong, nonatomic) NSWindow *window;
|
||||
#endif
|
||||
@property (strong, nonatomic) NSString *pcUuidToLoad;
|
||||
@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);
|
||||
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||
|
||||
+15
-18
@@ -18,19 +18,27 @@ static NSOperationQueue* mainQueue;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
static NSString* DB_NAME = @"Moonlight_tvOS.bin";
|
||||
#elif TARGET_OS_IPHONE
|
||||
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||
#else
|
||||
static NSString* DB_NAME = @"moonlight_mac.sqlite";
|
||||
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
|
||||
#if !TARGET_OS_TV
|
||||
UIApplicationShortcutItem* shortcut = [launchOptions valueForKey:UIApplicationLaunchOptionsShortcutItemKey];
|
||||
if (shortcut != nil) {
|
||||
_pcUuidToLoad = (NSString*)[shortcut.userInfo objectForKey:@"UUID"];
|
||||
}
|
||||
#endif
|
||||
return YES;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (void)application:(UIApplication *)application performActionForShortcutItem:(UIApplicationShortcutItem *)shortcutItem completionHandler:(void (^)(BOOL succeeded))completionHandler {
|
||||
_pcUuidToLoad = (NSString*)[shortcutItem.userInfo objectForKey:@"UUID"];
|
||||
_shortcutCompletionHandler = completionHandler;
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)applicationWillResignActive:(UIApplication *)application
|
||||
{
|
||||
// Sent when the application is about to move from active to inactive state. This can occur for certain types of temporary interruptions (such as an incoming phone call or SMS message) or when the user quits the application and it begins the transition to the background state.
|
||||
@@ -58,17 +66,6 @@ static NSString* DB_NAME = @"moonlight_mac.sqlite";
|
||||
// Saves changes in the application's managed object context before the application terminates.
|
||||
[self saveContext];
|
||||
}
|
||||
#else
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
// Insert code here to initialize your application
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
// Insert code here to tear down your application
|
||||
[self saveContext];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (void)saveContext
|
||||
{
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "CryptoManager.h"
|
||||
#import "mkcert.h"
|
||||
|
||||
#include <openssl/aes.h>
|
||||
#include <openssl/sha.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/pem.h>
|
||||
@@ -45,46 +44,47 @@ static NSData* p12 = nil;
|
||||
}
|
||||
|
||||
- (NSData*) aesEncrypt:(NSData*)data withKey:(NSData*)key {
|
||||
AES_KEY aesKey;
|
||||
AES_set_encrypt_key([key bytes], 128, &aesKey);
|
||||
int size = [self getEncryptSize:data];
|
||||
unsigned char* buffer = malloc(size);
|
||||
unsigned char* blockRoundedBuffer = calloc(1, size);
|
||||
memcpy(blockRoundedBuffer, [data bytes], [data length]);
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int ciphertextLen;
|
||||
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
|
||||
EVP_EncryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
NSMutableData* ciphertext = [NSMutableData dataWithLength:[data length]];
|
||||
EVP_EncryptUpdate(cipher,
|
||||
[ciphertext mutableBytes],
|
||||
&ciphertextLen,
|
||||
[data bytes],
|
||||
(int)[data length]);
|
||||
assert(ciphertextLen == [ciphertext length]);
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
// AES_encrypt only encrypts the first 16 bytes so iterate the entire buffer
|
||||
int blockOffset = 0;
|
||||
while (blockOffset < size) {
|
||||
AES_encrypt(blockRoundedBuffer + blockOffset, buffer + blockOffset, &aesKey);
|
||||
blockOffset += 16;
|
||||
}
|
||||
|
||||
NSData* encryptedData = [NSData dataWithBytes:buffer length:size];
|
||||
free(buffer);
|
||||
free(blockRoundedBuffer);
|
||||
return encryptedData;
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
- (NSData*) aesDecrypt:(NSData*)data withKey:(NSData*)key {
|
||||
AES_KEY aesKey;
|
||||
AES_set_decrypt_key([key bytes], 128, &aesKey);
|
||||
unsigned char* buffer = malloc([data length]);
|
||||
|
||||
// AES_decrypt only decrypts the first 16 bytes so iterate the entire buffer
|
||||
int blockOffset = 0;
|
||||
while (blockOffset < [data length]) {
|
||||
AES_decrypt([data bytes] + blockOffset, buffer + blockOffset, &aesKey);
|
||||
blockOffset += 16;
|
||||
}
|
||||
|
||||
NSData* decryptedData = [NSData dataWithBytes:buffer length:[data length]];
|
||||
free(buffer);
|
||||
return decryptedData;
|
||||
}
|
||||
EVP_CIPHER_CTX* cipher;
|
||||
int plaintextLen;
|
||||
|
||||
- (int) getEncryptSize:(NSData*)data {
|
||||
// the size is the length of the data ceiling to the nearest 16 bytes
|
||||
return (((int)[data length] + 15) / 16) * 16;
|
||||
cipher = EVP_CIPHER_CTX_new();
|
||||
|
||||
EVP_DecryptInit(cipher, EVP_aes_128_ecb(), [key bytes], NULL);
|
||||
EVP_CIPHER_CTX_set_padding(cipher, 0);
|
||||
|
||||
NSMutableData* plaintext = [NSMutableData dataWithLength:[data length]];
|
||||
EVP_DecryptUpdate(cipher,
|
||||
[plaintext mutableBytes],
|
||||
&plaintextLen,
|
||||
[data bytes],
|
||||
(int)[data length]);
|
||||
assert(plaintextLen == [plaintext length]);
|
||||
|
||||
EVP_CIPHER_CTX_free(cipher);
|
||||
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
+ (NSData*) pemToDer:(NSData*)pemCertBytes {
|
||||
@@ -227,13 +227,28 @@ static NSData* p12 = nil;
|
||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
||||
return NULL;
|
||||
}
|
||||
return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
|
||||
|
||||
#if (OPENSSL_VERSION_NUMBER < 0x10002000L)
|
||||
ASN1_BIT_STRING *asnSignature = x509->signature;
|
||||
#elif (OPENSSL_VERSION_NUMBER < 0x10100000L)
|
||||
ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, x509);
|
||||
#else
|
||||
const ASN1_BIT_STRING *asnSignature;
|
||||
X509_get0_signature(&asnSignature, NULL, x509);
|
||||
#endif
|
||||
|
||||
NSData* sig = [NSData dataWithBytes:asnSignature->data length:asnSignature->length];
|
||||
|
||||
X509_free(x509);
|
||||
|
||||
return sig;
|
||||
}
|
||||
|
||||
+ (NSData*)getKeyFromCertKeyPair:(CertKeyPair*)certKeyPair {
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
|
||||
PEM_write_bio_PrivateKey(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
|
||||
PEM_write_bio_PrivateKey_traditional(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
|
||||
+50
-83
@@ -5,19 +5,60 @@
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <openssl/pem.h>
|
||||
#include <openssl/conf.h>
|
||||
#include <openssl/pkcs12.h>
|
||||
|
||||
#ifndef OPENSSL_NO_ENGINE
|
||||
#include <openssl/engine.h>
|
||||
#endif
|
||||
#include <openssl/rsa.h>
|
||||
#include <openssl/x509.h>
|
||||
#include <openssl/rand.h>
|
||||
|
||||
static const int NUM_BITS = 2048;
|
||||
static const int SERIAL = 0;
|
||||
static const int NUM_YEARS = 10;
|
||||
static const int NUM_YEARS = 20;
|
||||
|
||||
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
|
||||
int add_ext(X509 *cert, int nid, char *value);
|
||||
void mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
X509* cert = X509_new();
|
||||
|
||||
EVP_PKEY_CTX* ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
|
||||
|
||||
EVP_PKEY_keygen_init(ctx);
|
||||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits);
|
||||
|
||||
// pk must be initialized on input
|
||||
EVP_PKEY* pk = NULL;
|
||||
EVP_PKEY_keygen(ctx, &pk);
|
||||
|
||||
EVP_PKEY_CTX_free(ctx);
|
||||
|
||||
X509_set_version(cert, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(cert), serial);
|
||||
#if OPENSSL_VERSION_NUMBER < 0x10100000L
|
||||
X509_gmtime_adj(X509_get_notBefore(cert), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(cert), 60 * 60 * 24 * 365 * years);
|
||||
#else
|
||||
ASN1_TIME* before = ASN1_STRING_dup(X509_get0_notBefore(cert));
|
||||
ASN1_TIME* after = ASN1_STRING_dup(X509_get0_notAfter(cert));
|
||||
|
||||
X509_gmtime_adj(before, 0);
|
||||
X509_gmtime_adj(after, 60 * 60 * 24 * 365 * years);
|
||||
|
||||
X509_set1_notBefore(cert, before);
|
||||
X509_set1_notAfter(cert, after);
|
||||
|
||||
ASN1_STRING_free(before);
|
||||
ASN1_STRING_free(after);
|
||||
#endif
|
||||
|
||||
X509_set_pubkey(cert, pk);
|
||||
|
||||
X509_NAME* name = X509_get_subject_name(cert);
|
||||
X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC,
|
||||
(const unsigned char*)"NVIDIA GameStream Client",
|
||||
-1, -1, 0);
|
||||
X509_set_issuer_name(cert, name);
|
||||
|
||||
X509_sign(cert, pk, EVP_sha256());
|
||||
|
||||
*x509p = cert;
|
||||
*pkeyp = pk;
|
||||
}
|
||||
|
||||
struct CertKeyPair generateCertKeyPair(void) {
|
||||
BIO *bio_err;
|
||||
@@ -25,32 +66,15 @@ struct CertKeyPair generateCertKeyPair(void) {
|
||||
EVP_PKEY *pkey = NULL;
|
||||
PKCS12 *p12 = NULL;
|
||||
|
||||
CRYPTO_mem_ctrl(CRYPTO_MEM_CHECK_ON);
|
||||
bio_err = BIO_new_fp(stderr, BIO_NOCLOSE);
|
||||
|
||||
SSLeay_add_all_algorithms();
|
||||
ERR_load_crypto_strings();
|
||||
|
||||
mkcert(&x509, &pkey, NUM_BITS, SERIAL, NUM_YEARS);
|
||||
|
||||
p12 = PKCS12_create("limelight", "GameStream", pkey, x509, NULL, 0, 0, 0, 0, 0);
|
||||
if (p12 == NULL) {
|
||||
printf("Error generating a valid PKCS12 certificate.\n");
|
||||
}
|
||||
|
||||
// Debug Print statements
|
||||
//RSA_print_fp(stdout, pkey->pkey.rsa, 0);
|
||||
//X509_print_fp(stdout, x509);
|
||||
//PEM_write_PUBKEY(stdout, pkey);
|
||||
//PEM_write_PrivateKey(stdout, pkey, NULL, NULL, 0, NULL, NULL);
|
||||
//PEM_write_X509(stdout, x509);
|
||||
|
||||
#ifndef OPENSSL_NO_ENGINE
|
||||
ENGINE_cleanup();
|
||||
#endif
|
||||
CRYPTO_cleanup_all_ex_data();
|
||||
|
||||
CRYPTO_mem_leaks(bio_err);
|
||||
BIO_free(bio_err);
|
||||
|
||||
return (CertKeyPair){x509, pkey, p12};
|
||||
@@ -61,60 +85,3 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
|
||||
EVP_PKEY_free(certKeyPair.pkey);
|
||||
PKCS12_free(certKeyPair.p12);
|
||||
}
|
||||
|
||||
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
X509 *x;
|
||||
EVP_PKEY *pk;
|
||||
RSA *rsa;
|
||||
X509_NAME *name = NULL;
|
||||
|
||||
if (*pkeyp == NULL) {
|
||||
if ((pk=EVP_PKEY_new()) == NULL) {
|
||||
return(0);
|
||||
}
|
||||
} else {
|
||||
pk = *pkeyp;
|
||||
}
|
||||
|
||||
if (*x509p == NULL) {
|
||||
if ((x = X509_new()) == NULL) {
|
||||
goto err;
|
||||
}
|
||||
} else {
|
||||
x = *x509p;
|
||||
}
|
||||
|
||||
rsa = RSA_generate_key(bits, RSA_F4, NULL, NULL);
|
||||
if (!EVP_PKEY_assign_RSA(pk, rsa)) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
X509_set_version(x, 2);
|
||||
ASN1_INTEGER_set(X509_get_serialNumber(x), serial);
|
||||
X509_gmtime_adj(X509_get_notBefore(x), 0);
|
||||
X509_gmtime_adj(X509_get_notAfter(x), (long)60*60*24*365*years);
|
||||
X509_set_pubkey(x, pk);
|
||||
|
||||
name = X509_get_subject_name(x);
|
||||
|
||||
/* This function creates and adds the entry, working out the
|
||||
* correct string type and performing checks on its length.
|
||||
*/
|
||||
X509_NAME_add_entry_by_txt(name,"CN", MBSTRING_ASC, (unsigned char*)"NVIDIA GameStream Client", -1, -1, 0);
|
||||
|
||||
/* Its self signed so set the issuer name to be the same as the
|
||||
* subject.
|
||||
*/
|
||||
X509_set_issuer_name(x, name);
|
||||
|
||||
if (!X509_sign(x, pk, EVP_sha256())) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
*x509p = x;
|
||||
*pkeyp = pk;
|
||||
|
||||
return(1);
|
||||
err:
|
||||
return(0);
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
remote:(BOOL)streamingRemotely
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr;
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
|
||||
@@ -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];
|
||||
});
|
||||
}
|
||||
|
||||
@@ -58,12 +57,14 @@
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
remote:(BOOL)streamingRemotely
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr {
|
||||
enableHdr:(BOOL)enableHdr
|
||||
btMouseSupport:(BOOL)btMouseSupport
|
||||
absoluteTouchMode:(BOOL)absoluteTouchMode
|
||||
statsOverlay:(BOOL)statsOverlay {
|
||||
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Settings* settingsToSave = [self retrieveSettings];
|
||||
@@ -72,12 +73,14 @@
|
||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
||||
settingsToSave.streamingRemotely = streamingRemotely;
|
||||
settingsToSave.optimizeGames = optimizeGames;
|
||||
settingsToSave.multiController = multiController;
|
||||
settingsToSave.playAudioOnPC = audioOnPC;
|
||||
settingsToSave.useHevc = useHevc;
|
||||
settingsToSave.enableHdr = enableHdr;
|
||||
settingsToSave.btMouseSupport = btMouseSupport;
|
||||
settingsToSave.absoluteTouchMode = absoluteTouchMode;
|
||||
settingsToSave.statsOverlay = statsOverlay;
|
||||
|
||||
[self saveData];
|
||||
}];
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -41,7 +43,7 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:[App class]]) {
|
||||
if (![object isKindOfClass:[self class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -11,24 +11,24 @@
|
||||
|
||||
@interface TemporaryHost : NSObject
|
||||
|
||||
@property (nonatomic) State state;
|
||||
@property (nonatomic) PairState pairState;
|
||||
@property (nonatomic, nullable) NSString * activeAddress;
|
||||
@property (nonatomic, nullable) NSString * currentGame;
|
||||
@property (atomic) State state;
|
||||
@property (atomic) PairState pairState;
|
||||
@property (atomic, nullable, retain) NSString * activeAddress;
|
||||
@property (atomic, nullable, retain) NSString * currentGame;
|
||||
|
||||
@property (nonatomic, nullable, retain) NSData *serverCert;
|
||||
@property (nonatomic, nullable, retain) NSString *address;
|
||||
@property (nonatomic, nullable, retain) NSString *externalAddress;
|
||||
@property (nonatomic, nullable, retain) NSString *localAddress;
|
||||
@property (nonatomic, nullable, retain) NSString *ipv6Address;
|
||||
@property (nonatomic, nullable, retain) NSString *mac;
|
||||
@property (nonatomic) int serverCodecModeSupport;
|
||||
@property (atomic, nullable, retain) NSData *serverCert;
|
||||
@property (atomic, nullable, retain) NSString *address;
|
||||
@property (atomic, nullable, retain) NSString *externalAddress;
|
||||
@property (atomic, nullable, retain) NSString *localAddress;
|
||||
@property (atomic, nullable, retain) NSString *ipv6Address;
|
||||
@property (atomic, nullable, retain) NSString *mac;
|
||||
@property (atomic) int serverCodecModeSupport;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, retain) NSString *name;
|
||||
@property (nonatomic, retain) NSString *uuid;
|
||||
@property (nonatomic, retain) NSMutableSet *appList;
|
||||
@property (atomic, retain) NSString *name;
|
||||
@property (atomic, retain) NSString *uuid;
|
||||
@property (atomic, retain) NSSet *appList;
|
||||
|
||||
- (id) initFromHost:(Host*)host;
|
||||
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
return YES;
|
||||
}
|
||||
|
||||
if (![object isKindOfClass:[Host class]]) {
|
||||
if (![object isKindOfClass:[self class]]) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,12 +18,14 @@
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic) BOOL streamingRemotely;
|
||||
@property (nonatomic) BOOL useHevc;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
@property (nonatomic) BOOL btMouseSupport;
|
||||
@property (nonatomic) BOOL absoluteTouchMode;
|
||||
@property (nonatomic) BOOL statsOverlay;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
|
||||
@@ -17,26 +17,30 @@
|
||||
self.parent = settings;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
NSInteger _bitrate = [[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"];
|
||||
NSInteger _framerate = [[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"];
|
||||
|
||||
if (_bitrate) {
|
||||
self.bitrate = [NSNumber numberWithInteger:_bitrate];
|
||||
} else {
|
||||
self.bitrate = [NSNumber numberWithInteger:20000];
|
||||
// Apply default values from our Root.plist
|
||||
NSString* settingsBundle = [[NSBundle mainBundle] pathForResource:@"Settings" ofType:@"bundle"];
|
||||
NSDictionary* settingsData = [NSDictionary dictionaryWithContentsOfFile:[settingsBundle stringByAppendingPathComponent:@"Root.plist"]];
|
||||
NSArray* preferences = [settingsData objectForKey:@"PreferenceSpecifiers"];
|
||||
NSMutableDictionary* defaultsToRegister = [[NSMutableDictionary alloc] initWithCapacity:[preferences count]];
|
||||
for (NSDictionary* prefSpecification in preferences) {
|
||||
NSString* key = [prefSpecification objectForKey:@"Key"];
|
||||
if (key != nil) {
|
||||
[defaultsToRegister setObject:[prefSpecification objectForKey:@"DefaultValue"] forKey:key];
|
||||
}
|
||||
}
|
||||
|
||||
if (_framerate) {
|
||||
self.framerate = [NSNumber numberWithInteger:_framerate];
|
||||
} else {
|
||||
self.framerate = [NSNumber numberWithInteger:60];
|
||||
}
|
||||
|
||||
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"] || NO;
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"] || NO;
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"] || NO;
|
||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"] || YES;
|
||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"] || YES;
|
||||
[[NSUserDefaults standardUserDefaults] registerDefaults:defaultsToRegister];
|
||||
|
||||
self.bitrate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"]];
|
||||
assert([self.bitrate intValue] != 0);
|
||||
self.framerate = [NSNumber numberWithInteger:[[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"]];
|
||||
assert([self.framerate intValue] != 0);
|
||||
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"];
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
|
||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"];
|
||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"];
|
||||
self.btMouseSupport = [[NSUserDefaults standardUserDefaults] boolForKey:@"btMouseSupport"];
|
||||
self.statsOverlay = [[NSUserDefaults standardUserDefaults] boolForKey:@"statsOverlay"];
|
||||
|
||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||
switch (_screenSize) {
|
||||
@@ -44,15 +48,16 @@
|
||||
self.height = [NSNumber numberWithInteger:720];
|
||||
self.width = [NSNumber numberWithInteger:1280];
|
||||
break;
|
||||
case 1:
|
||||
self.height = [NSNumber numberWithInteger:1080];
|
||||
self.width = [NSNumber numberWithInteger:1920];
|
||||
break;
|
||||
case 2:
|
||||
self.height = [NSNumber numberWithInteger:2160];
|
||||
self.width = [NSNumber numberWithInteger:3840];
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
self.height = [NSNumber numberWithInteger:1080];
|
||||
self.width = [NSNumber numberWithInteger:1920];
|
||||
break;
|
||||
abort();
|
||||
}
|
||||
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
|
||||
#else
|
||||
@@ -66,9 +71,11 @@
|
||||
self.optimizeGames = settings.optimizeGames;
|
||||
self.multiController = settings.multiController;
|
||||
self.onscreenControls = settings.onscreenControls;
|
||||
self.btMouseSupport = settings.btMouseSupport;
|
||||
self.absoluteTouchMode = settings.absoluteTouchMode;
|
||||
self.statsOverlay = settings.statsOverlay;
|
||||
#endif
|
||||
self.uniqueId = settings.uniqueId;
|
||||
self.streamingRemotely = settings.streamingRemotely;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
Binary file not shown.
+13
-11
@@ -5,11 +5,13 @@
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœ]‘]!„ß{Š^ÀÊ”Àâ1<‚Ùøó êý…E␍ »ÛN‡Yp°µƒmª‰×=)´³×•÷§À×7iØ·¸Hbä"‹= ¯3_\W«FÆ!K©%ðÁ½ÛñÎX¤@;$Ú®‹ ”mÊê@Œ¬Ç¿ ‰µµ+¸“!ïd�±ùË}œ#¡TÙ;7_Û«†,ÚLÙ˜EˆMHS~»ì8Ðà—î�$ëô¾±ÍX™F—ØÙ´<òŸ\W¾Í¤MlæÜûþeÓeÇÿÎé¦Lg7
|
||||
xœ]‘[ƒ Eÿ³Šl )y¸Œ.¡ÃTû¡µûŸi€ Õa9×›�ÑȺ±l.zÌ+|ÀTm›ñþ48a”ä_°äÑ&&ë‘SØ^8cYÕÍÅíl"‹«Æ5ºá‚v¤�’"{
|
||||
ò<9¶�j�7Ö!Db ×,Ú�–PÊbåÐjt¤•øzÅÚœÔ:ƒº,'á½â‚ýÒ‡ wRÁù‘Ê|µ8)’w
|
||||
-{GÍÑsµ™�æüÞG^’íosû»£MÅkäøà=h_
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
212
|
||||
214
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
@@ -22,7 +24,7 @@ endobj
|
||||
2 0 obj
|
||||
<< /Type /Page % 1
|
||||
/Parent 1 0 R
|
||||
/MediaBox [ 0 0 225 225 ]
|
||||
/MediaBox [ 0 0 375 375 ]
|
||||
/Contents 4 0 R
|
||||
/Group <<
|
||||
/Type /Group
|
||||
@@ -41,7 +43,7 @@ endobj
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Producer (cairo 1.16.0 (https://cairographics.org))
|
||||
/CreationDate (D:20190830180928-07'00)
|
||||
/CreationDate (D:20190922201805-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
@@ -52,18 +54,18 @@ endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000648 00000 n
|
||||
0000000430 00000 n
|
||||
0000000326 00000 n
|
||||
0000000650 00000 n
|
||||
0000000432 00000 n
|
||||
0000000328 00000 n
|
||||
0000000015 00000 n
|
||||
0000000304 00000 n
|
||||
0000000713 00000 n
|
||||
0000000829 00000 n
|
||||
0000000306 00000 n
|
||||
0000000715 00000 n
|
||||
0000000831 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
881
|
||||
883
|
||||
%%EOF
|
||||
|
||||
+11
-11
@@ -5,11 +5,11 @@
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœeŒÑ Ã0Dÿ5Å-Uªì(kt„Rhò¤Ýª8µ �t︓Bb�å¯B)öù̸=ó—Œõž1fã¸òä�W˜sW½P’jçjXbó†kG Ý’rî%c?õì=ÓãÍ?ÜðÒ~Óƒ~®L+·
|
||||
xœUMA€ »ïý€¸� ßð ÆD=ÈAý"L4š&[»µ›€3ÉE<cJ´“ àXÐŽŒå¤`¬‡8)-A:gúWo(ÜšÂVŠ:Õ !ª±n«bμú”?©ØB*žL¸Ü¶ðÿUS3␍tH(å
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
115
|
||||
110
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
@@ -21,7 +21,7 @@ endobj
|
||||
2 0 obj
|
||||
<< /Type /Page % 1
|
||||
/Parent 1 0 R
|
||||
/MediaBox [ 0 0 75 75 ]
|
||||
/MediaBox [ 0 0 150 150 ]
|
||||
/Contents 4 0 R
|
||||
/Group <<
|
||||
/Type /Group
|
||||
@@ -40,7 +40,7 @@ endobj
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Producer (cairo 1.16.0 (https://cairographics.org))
|
||||
/CreationDate (D:20190830180932-07'00)
|
||||
/CreationDate (D:20190922201819-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
@@ -51,18 +51,18 @@ endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000517 00000 n
|
||||
0000000301 00000 n
|
||||
0000000229 00000 n
|
||||
0000000514 00000 n
|
||||
0000000296 00000 n
|
||||
0000000224 00000 n
|
||||
0000000015 00000 n
|
||||
0000000207 00000 n
|
||||
0000000582 00000 n
|
||||
0000000698 00000 n
|
||||
0000000202 00000 n
|
||||
0000000579 00000 n
|
||||
0000000695 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
750
|
||||
747
|
||||
%%EOF
|
||||
|
||||
Binary file not shown.
@@ -1,21 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "settings.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
%PDF-1.5
|
||||
%µí®û
|
||||
4 0 obj
|
||||
<< /Length 5 0 R
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœeTIŽÜ0¼óú@‰Z,=#OÌÌA}Hò ,.== °»d®Å¢j*òûVåq�t?èÕ„ßï÷ôýgIï¨]y¤V3�ôHç2Ù¡?¿ÁøvÐKÞm¹¥Þòåö÷ƒþ|=òp,EØÏ„ö÷N4vnbjÏGª#OxRå\ÇL¯68Z+€™ß‰�p´÷ä¼*G”iÆî3ÃÕ}ÓQ¯Þã ^Ê‘ûÜÿÄ<O¬ER„úR\¸Jï-ï2�‘G
|
||||
´2×␍OA nŸ;J^CXFR¾V»!PïJ” 8 šÂ�øv8␍Ö,G³Î␍<Q°À.¾×¥VÛ�|ÄX¯Üa\2zí37)qT+fä‚䌗iGJ‚«¦ílyVîÒÏÖK�iZã¦Wû¦ˆ Ζ+¿L·±5jX�~µP‚J«šH�)»~6xïè␍^¼´@i°ŒÎE_wbަ.e‹«%V^PsÙg.ÙâêŒê´^ºrw€J0Ê„—°$Ã!@’ti^[•µiZØÅ€¬»‡PADt?áTúÃqê"zÔáô¤B�횕Ԕ Šz[±²naä·÷*ô Ë "·ÊÁ+Ã$úÇ;Hæ®*ò°2�á@qlÂóÙÁ³w‚␍WªÒωѣV9vYø2ºhžW„«Ê0™1s7pAz(—+©\ÎÞÃ�ÚU=êD±
|
||||
lèk"␍š}�Ę•-_²—m }%ßO0ë›®½Ïë»-X·Ëö¾Ùú€yÖ␍¶üÖZÛy^)&†¸nL(¯WÑÿWÓý ¿¾?1
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
608
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/ExtGState <<
|
||||
/a0 << /CA 1 /ca 1 >>
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Page % 1
|
||||
/Parent 1 0 R
|
||||
/MediaBox [ 0 0 75 75 ]
|
||||
/Contents 4 0 R
|
||||
/Group <<
|
||||
/Type /Group
|
||||
/S /Transparency
|
||||
/I true
|
||||
/CS /DeviceRGB
|
||||
>>
|
||||
/Resources 3 0 R
|
||||
>>
|
||||
endobj
|
||||
1 0 obj
|
||||
<< /Type /Pages
|
||||
/Kids [ 2 0 R ]
|
||||
/Count 1
|
||||
>>
|
||||
endobj
|
||||
6 0 obj
|
||||
<< /Producer (cairo 1.16.0 (https://cairographics.org))
|
||||
/CreationDate (D:20190830202122-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000001010 00000 n
|
||||
0000000794 00000 n
|
||||
0000000722 00000 n
|
||||
0000000015 00000 n
|
||||
0000000700 00000 n
|
||||
0000001075 00000 n
|
||||
0000001191 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
1243
|
||||
%%EOF
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -6,7 +6,10 @@
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "HapticContext.h"
|
||||
|
||||
@import GameController;
|
||||
@import CoreHaptics;
|
||||
|
||||
@interface Controller : NSObject
|
||||
|
||||
@@ -14,13 +17,15 @@
|
||||
@property (nonatomic) int playerIndex;
|
||||
@property (nonatomic) int lastButtonFlags;
|
||||
@property (nonatomic) int emulatingButtonFlags;
|
||||
@property (nonatomic) int supportedEmulationFlags;
|
||||
@property (nonatomic) unsigned char lastLeftTrigger;
|
||||
@property (nonatomic) unsigned char lastRightTrigger;
|
||||
@property (nonatomic) short lastLeftStickX;
|
||||
@property (nonatomic) short lastLeftStickY;
|
||||
@property (nonatomic) short lastRightStickX;
|
||||
@property (nonatomic) short lastRightStickY;
|
||||
@property (nonatomic) unsigned short lowFreqMotor;
|
||||
@property (nonatomic) unsigned short highFreqMotor;
|
||||
|
||||
@property (nonatomic) HapticContext* _Nullable lowFreqMotor;
|
||||
@property (nonatomic) HapticContext* _Nullable highFreqMotor;
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,35 +6,32 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
// Swift
|
||||
#if !TARGET_OS_IPHONE
|
||||
#import "Gamepad.h"
|
||||
#endif
|
||||
#import "StreamConfiguration.h"
|
||||
#import "Controller.h"
|
||||
|
||||
@class OnScreenControls;
|
||||
|
||||
@protocol InputPresenceDelegate <NSObject>
|
||||
|
||||
- (void) gamepadPresenceChanged;
|
||||
- (void) mousePresenceChanged;
|
||||
|
||||
@end
|
||||
|
||||
@interface ControllerSupport : NSObject
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig;
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<InputPresenceDelegate>)delegate;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
||||
-(void) cleanup;
|
||||
-(Controller*) getOscController;
|
||||
#else
|
||||
-(void) assignGamepad:(struct Gamepad_device *)gamepad;
|
||||
-(void) removeGamepad:(struct Gamepad_device *)gamepad;
|
||||
-(NSMutableDictionary*) getControllers;
|
||||
#endif
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
|
||||
-(void) updateLeftTrigger:(Controller*)controller left:(char)left;
|
||||
-(void) updateRightTrigger:(Controller*)controller right:(char)right;
|
||||
-(void) updateTriggers:(Controller*)controller left:(char)left right:(char)right;
|
||||
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left;
|
||||
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right;
|
||||
-(void) updateTriggers:(Controller*)controller left:(unsigned char)left right:(unsigned char)right;
|
||||
|
||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags;
|
||||
-(void) setButtonFlag:(Controller*)controller flags:(int)flags;
|
||||
@@ -46,7 +43,6 @@
|
||||
|
||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
||||
|
||||
@property (nonatomic, strong) id connectObserver;
|
||||
@property (nonatomic, strong) id disconnectObserver;
|
||||
-(NSUInteger) getConnectedGamepadCount;
|
||||
|
||||
@end
|
||||
|
||||
+317
-126
@@ -10,10 +10,6 @@
|
||||
#import "Controller.h"
|
||||
|
||||
#import "OnScreenControls.h"
|
||||
#if !TARGET_OS_IPHONE
|
||||
#import "Gamepad.h"
|
||||
#import "Control.h"
|
||||
#endif
|
||||
|
||||
#import "DataManager.h"
|
||||
#include "Limelight.h"
|
||||
@@ -21,10 +17,23 @@
|
||||
@import GameController;
|
||||
@import AudioToolbox;
|
||||
|
||||
static const double MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
@implementation ControllerSupport {
|
||||
id _controllerConnectObserver;
|
||||
id _controllerDisconnectObserver;
|
||||
id _mouseConnectObserver;
|
||||
id _mouseDisconnectObserver;
|
||||
id _keyboardConnectObserver;
|
||||
id _keyboardDisconnectObserver;
|
||||
|
||||
NSLock *_controllerStreamLock;
|
||||
NSMutableDictionary *_controllers;
|
||||
NSTimer *_rumbleTimer;
|
||||
id<InputPresenceDelegate> _presenceDelegate;
|
||||
|
||||
float accumulatedDeltaX;
|
||||
float accumulatedDeltaY;
|
||||
float accumulatedScrollY;
|
||||
|
||||
OnScreenControls *_osc;
|
||||
|
||||
@@ -44,20 +53,6 @@
|
||||
#define UPDATE_BUTTON_FLAG(controller, x, y) \
|
||||
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
|
||||
|
||||
-(void) rumbleController: (Controller*)controller
|
||||
{
|
||||
#if 0
|
||||
// Only vibrate if the amplitude is large enough
|
||||
if (controller.lowFreqMotor > 0x5000 || controller.highFreqMotor > 0x5000) {
|
||||
// If the gamepad is nil (on-screen controls) or it's attached to the device,
|
||||
// then vibrate the device itself
|
||||
if (controller.gamepad == nil || [controller.gamepad isAttachedToDevice]) {
|
||||
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||
{
|
||||
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||
@@ -70,12 +65,8 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the motor levels for the rumble timer to grab next iteration
|
||||
controller.lowFreqMotor = lowFreqMotor;
|
||||
controller.highFreqMotor = highFreqMotor;
|
||||
|
||||
// Rumble now to ensure short vibrations aren't missed
|
||||
[self rumbleController:controller];
|
||||
[controller.lowFreqMotor setMotorAmplitude:lowFreqMotor];
|
||||
[controller.highFreqMotor setMotorAmplitude:highFreqMotor];
|
||||
}
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
|
||||
@@ -94,21 +85,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(void) updateLeftTrigger:(Controller*)controller left:(char)left
|
||||
-(void) updateLeftTrigger:(Controller*)controller left:(unsigned char)left
|
||||
{
|
||||
@synchronized(controller) {
|
||||
controller.lastLeftTrigger = left;
|
||||
}
|
||||
}
|
||||
|
||||
-(void) updateRightTrigger:(Controller*)controller right:(char)right
|
||||
-(void) updateRightTrigger:(Controller*)controller right:(unsigned char)right
|
||||
{
|
||||
@synchronized(controller) {
|
||||
controller.lastRightTrigger = right;
|
||||
}
|
||||
}
|
||||
|
||||
-(void) updateTriggers:(Controller*) controller left:(char)left right:(char)right
|
||||
-(void) updateTriggers:(Controller*) controller left:(unsigned char)left right:(unsigned char)right
|
||||
{
|
||||
@synchronized(controller) {
|
||||
controller.lastLeftTrigger = left;
|
||||
@@ -118,17 +109,26 @@
|
||||
|
||||
-(void) handleSpecialCombosReleased:(Controller*)controller releasedButtons:(int)releasedButtons
|
||||
{
|
||||
if ((controller.emulatingButtonFlags & EMULATING_SELECT) &&
|
||||
((releasedButtons & LB_FLAG) || (releasedButtons & PLAY_FLAG))) {
|
||||
if ((controller.emulatingButtonFlags & EMULATING_SELECT) && (releasedButtons & (LB_FLAG | PLAY_FLAG))) {
|
||||
controller.lastButtonFlags &= ~BACK_FLAG;
|
||||
controller.emulatingButtonFlags &= ~EMULATING_SELECT;
|
||||
}
|
||||
if ((controller.emulatingButtonFlags & EMULATING_SPECIAL) &&
|
||||
((releasedButtons & RB_FLAG) || (releasedButtons & PLAY_FLAG) ||
|
||||
(releasedButtons & BACK_FLAG))) {
|
||||
controller.lastButtonFlags &= ~SPECIAL_FLAG;
|
||||
controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
|
||||
|
||||
if (controller.emulatingButtonFlags & EMULATING_SPECIAL) {
|
||||
// If Select is emulated, we use RB+Start to emulate special, otherwise we use Start+Select
|
||||
if (controller.supportedEmulationFlags & EMULATING_SELECT) {
|
||||
if (releasedButtons & (RB_FLAG | PLAY_FLAG)) {
|
||||
controller.lastButtonFlags &= ~SPECIAL_FLAG;
|
||||
controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if (releasedButtons & (BACK_FLAG | PLAY_FLAG)) {
|
||||
controller.lastButtonFlags &= ~SPECIAL_FLAG;
|
||||
controller.emulatingButtonFlags &= ~EMULATING_SPECIAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void) handleSpecialCombosPressed:(Controller*)controller pressedButtons:(int)pressedButtons
|
||||
@@ -137,18 +137,31 @@
|
||||
if (controller.lastButtonFlags & PLAY_FLAG) {
|
||||
// If LB and start are down, trigger select
|
||||
if (controller.lastButtonFlags & LB_FLAG) {
|
||||
controller.lastButtonFlags |= BACK_FLAG;
|
||||
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
|
||||
controller.emulatingButtonFlags |= EMULATING_SELECT;
|
||||
if (controller.supportedEmulationFlags & EMULATING_SELECT) {
|
||||
controller.lastButtonFlags |= BACK_FLAG;
|
||||
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | LB_FLAG));
|
||||
controller.emulatingButtonFlags |= EMULATING_SELECT;
|
||||
}
|
||||
}
|
||||
// If (RB or select) and start are down, trigger special
|
||||
else if ((controller.lastButtonFlags & RB_FLAG) || (controller.lastButtonFlags & BACK_FLAG)) {
|
||||
controller.lastButtonFlags |= SPECIAL_FLAG;
|
||||
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG | BACK_FLAG));
|
||||
controller.emulatingButtonFlags |= EMULATING_SPECIAL;
|
||||
else if (controller.supportedEmulationFlags & EMULATING_SPECIAL) {
|
||||
// If Select is emulated too, use RB+Start to emulate special
|
||||
if (controller.supportedEmulationFlags & EMULATING_SELECT) {
|
||||
if (controller.lastButtonFlags & RB_FLAG) {
|
||||
controller.lastButtonFlags |= SPECIAL_FLAG;
|
||||
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | RB_FLAG));
|
||||
controller.emulatingButtonFlags |= EMULATING_SPECIAL;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// If Select is physical, use Start+Select to emulate special
|
||||
if (controller.lastButtonFlags & BACK_FLAG) {
|
||||
controller.lastButtonFlags |= SPECIAL_FLAG;
|
||||
controller.lastButtonFlags &= ~(pressedButtons & (PLAY_FLAG | BACK_FLAG));
|
||||
controller.emulatingButtonFlags |= EMULATING_SPECIAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
|
||||
@@ -194,6 +207,15 @@
|
||||
[_controllerStreamLock unlock];
|
||||
}
|
||||
|
||||
+(BOOL) hasKeyboardOrMouse {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
return GCMouse.mice.count > 0 || GCKeyboard.coalescedKeyboard != nil;
|
||||
}
|
||||
else {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
@@ -203,6 +225,13 @@
|
||||
controller.controllerPausedHandler = NULL;
|
||||
|
||||
if (controller.extendedGamepad != NULL) {
|
||||
// Re-enable system gestures on the gamepad buttons now
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
for (GCControllerElement* element in controller.extendedGamepad.allElements) {
|
||||
element.preferredSystemGestureState = GCSystemGestureStateEnabled;
|
||||
}
|
||||
}
|
||||
|
||||
controller.extendedGamepad.valueChangedHandler = NULL;
|
||||
}
|
||||
else if (controller.gamepad != NULL) {
|
||||
@@ -211,15 +240,36 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(void) initializeControllerHaptics:(Controller*) controller
|
||||
{
|
||||
controller.lowFreqMotor = [HapticContext createContextForLowFreqMotor:controller.gamepad];
|
||||
controller.highFreqMotor = [HapticContext createContextForHighFreqMotor:controller.gamepad];
|
||||
}
|
||||
|
||||
-(void) cleanupControllerHaptics:(Controller*) controller
|
||||
{
|
||||
[controller.lowFreqMotor cleanup];
|
||||
[controller.highFreqMotor cleanup];
|
||||
}
|
||||
|
||||
-(void) registerControllerCallbacks:(GCController*) controller
|
||||
{
|
||||
if (controller != NULL) {
|
||||
// On iOS 13, we want to use the new buttonMenu property which lets users hold down Start.
|
||||
// On prior versions, we must use the controllerPausedHandler.
|
||||
// iOS 13 allows the Start button to behave like a normal button, however
|
||||
// older MFi controllers can send an instant down+up event for the start button
|
||||
// which means the button will not be down long enough to register on the PC.
|
||||
// To work around this issue, use the old controllerPausedHandler if the controller
|
||||
// doesn't have a Select button (which indicates it probably doesn't have a proper
|
||||
// Start button either).
|
||||
BOOL useLegacyPausedHandler = YES;
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
// Nothing - buttonMenu is already handled in valueChangedHandler
|
||||
if (controller.extendedGamepad != nil &&
|
||||
controller.extendedGamepad.buttonOptions != nil) {
|
||||
useLegacyPausedHandler = NO;
|
||||
}
|
||||
}
|
||||
else {
|
||||
|
||||
if (useLegacyPausedHandler) {
|
||||
controller.controllerPausedHandler = ^(GCController *controller) {
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
@@ -238,11 +288,19 @@
|
||||
}
|
||||
|
||||
if (controller.extendedGamepad != NULL) {
|
||||
// Disable system gestures on the gamepad to avoid interfering
|
||||
// with in-game controller actions
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
for (GCControllerElement* element in controller.extendedGamepad.allElements) {
|
||||
element.preferredSystemGestureState = GCSystemGestureStateDisabled;
|
||||
}
|
||||
}
|
||||
|
||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
short leftStickX, leftStickY;
|
||||
short rightStickX, rightStickY;
|
||||
char leftTrigger, rightTrigger;
|
||||
unsigned char leftTrigger, rightTrigger;
|
||||
|
||||
UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
|
||||
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
|
||||
@@ -268,12 +326,20 @@
|
||||
}
|
||||
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
// Menu button is mandatory, so no need to check for nil
|
||||
UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, gamepad.buttonMenu.pressed);
|
||||
|
||||
// Options button is optional (only present on Xbox One S and PS4 gamepads)
|
||||
if (gamepad.buttonOptions != nil) {
|
||||
UPDATE_BUTTON_FLAG(limeController, BACK_FLAG, gamepad.buttonOptions.pressed);
|
||||
|
||||
// For older MFi gamepads, the menu button will already be handled by
|
||||
// the controllerPausedHandler.
|
||||
UPDATE_BUTTON_FLAG(limeController, PLAY_FLAG, gamepad.buttonMenu.pressed);
|
||||
}
|
||||
}
|
||||
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
// Home/Guide button is optional (only present on Xbox One S and PS4 gamepads)
|
||||
if (gamepad.buttonHome != nil) {
|
||||
UPDATE_BUTTON_FLAG(limeController, SPECIAL_FLAG, gamepad.buttonHome.pressed);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -316,6 +382,76 @@
|
||||
}
|
||||
}
|
||||
|
||||
-(void) unregisterMouseCallbacks:(GCMouse*)mouse API_AVAILABLE(ios(14.0)) {
|
||||
mouse.mouseInput.mouseMovedHandler = nil;
|
||||
|
||||
mouse.mouseInput.leftButton.pressedChangedHandler = nil;
|
||||
mouse.mouseInput.middleButton.pressedChangedHandler = nil;
|
||||
mouse.mouseInput.rightButton.pressedChangedHandler = nil;
|
||||
|
||||
for (GCControllerButtonInput* auxButton in mouse.mouseInput.auxiliaryButtons) {
|
||||
auxButton.pressedChangedHandler = nil;
|
||||
}
|
||||
}
|
||||
|
||||
-(void) registerMouseCallbacks:(GCMouse*) mouse API_AVAILABLE(ios(14.0)) {
|
||||
mouse.mouseInput.mouseMovedHandler = ^(GCMouseInput * _Nonnull mouse, float deltaX, float deltaY) {
|
||||
self->accumulatedDeltaX += deltaX / MOUSE_SPEED_DIVISOR;
|
||||
self->accumulatedDeltaY += -deltaY / MOUSE_SPEED_DIVISOR;
|
||||
|
||||
short truncatedDeltaX = (short)self->accumulatedDeltaX;
|
||||
short truncatedDeltaY = (short)self->accumulatedDeltaY;
|
||||
|
||||
if (truncatedDeltaX != 0 || truncatedDeltaY != 0) {
|
||||
LiSendMouseMoveEvent(truncatedDeltaX, truncatedDeltaY);
|
||||
|
||||
self->accumulatedDeltaX -= truncatedDeltaX;
|
||||
self->accumulatedDeltaY -= truncatedDeltaY;
|
||||
}
|
||||
};
|
||||
|
||||
mouse.mouseInput.leftButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
|
||||
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
};
|
||||
mouse.mouseInput.middleButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
|
||||
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_MIDDLE);
|
||||
};
|
||||
mouse.mouseInput.rightButton.pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
|
||||
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
};
|
||||
|
||||
if (mouse.mouseInput.auxiliaryButtons != nil) {
|
||||
if (mouse.mouseInput.auxiliaryButtons.count >= 1) {
|
||||
mouse.mouseInput.auxiliaryButtons[0].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
|
||||
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X1);
|
||||
};
|
||||
}
|
||||
if (mouse.mouseInput.auxiliaryButtons.count >= 2) {
|
||||
mouse.mouseInput.auxiliaryButtons[1].pressedChangedHandler = ^(GCControllerButtonInput * _Nonnull button, float value, BOOL pressed) {
|
||||
LiSendMouseButtonEvent(pressed ? BUTTON_ACTION_PRESS : BUTTON_ACTION_RELEASE, BUTTON_X2);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// We use UIPanGestureRecognizer on iPadOS because it allows us to distinguish
|
||||
// between discrete and continuous scroll events and also works around a bug
|
||||
// in iPadOS 15 where discrete scroll events are dropped. tvOS only supports
|
||||
// GCMouse for mice, so we will have to just use it and hope for the best.
|
||||
#if TARGET_OS_TV
|
||||
mouse.mouseInput.scroll.yAxis.valueChangedHandler = ^(GCControllerAxisInput * _Nonnull axis, float value) {
|
||||
self->accumulatedScrollY += value;
|
||||
|
||||
short truncatedScrollY = (short)self->accumulatedScrollY;
|
||||
|
||||
if (truncatedScrollY != 0) {
|
||||
LiSendHighResScrollEvent(truncatedScrollY * 20);
|
||||
|
||||
self->accumulatedScrollY -= truncatedScrollY;
|
||||
}
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void) updateAutoOnScreenControlMode
|
||||
{
|
||||
// Auto on-screen control support may not be enabled
|
||||
@@ -354,9 +490,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// If we didn't find a gamepad present and we have a keyboard or mouse, turn
|
||||
// the on-screen controls off to get the overlays out of the way.
|
||||
if (level == OnScreenControlsLevelFull && [ControllerSupport hasKeyboardOrMouse]) {
|
||||
level = OnScreenControlsLevelOff;
|
||||
|
||||
// Ensure the virtual gamepad disappears to avoid confusing some games.
|
||||
// If the mouse and keyboard disconnect later, it will reappear when the
|
||||
// first OSC input is received.
|
||||
LiSendMultiControllerEvent(0, 0, 0, 0, 0, 0, 0, 0, 0);
|
||||
}
|
||||
|
||||
[_osc setLevel:level];
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
|
||||
@@ -382,7 +527,27 @@
|
||||
limeController.playerIndex = i;
|
||||
}
|
||||
|
||||
limeController.supportedEmulationFlags = EMULATING_SPECIAL | EMULATING_SELECT;
|
||||
limeController.gamepad = controller;
|
||||
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
if (controller.extendedGamepad != nil &&
|
||||
controller.extendedGamepad.buttonOptions != nil) {
|
||||
// Disable select button emulation since we have a physical select button
|
||||
limeController.supportedEmulationFlags &= ~EMULATING_SELECT;
|
||||
}
|
||||
}
|
||||
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
if (controller.extendedGamepad != nil &&
|
||||
controller.extendedGamepad.buttonHome != nil) {
|
||||
// Disable special button emulation since we have a physical special button
|
||||
limeController.supportedEmulationFlags &= ~EMULATING_SPECIAL;
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare controller haptics for use
|
||||
[self initializeControllerHaptics:limeController];
|
||||
|
||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
@@ -392,40 +557,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
-(Controller*) getOscController {
|
||||
return _player0osc;
|
||||
}
|
||||
#else
|
||||
-(NSMutableDictionary*) getControllers {
|
||||
return _controllers;
|
||||
}
|
||||
|
||||
-(void) assignGamepad:(struct Gamepad_device *)gamepad {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!(_controllerNumbers & (1 << i))) {
|
||||
_controllerNumbers |= (1 << i);
|
||||
gamepad->deviceID = i;
|
||||
NSLog(@"Gamepad device id: %u assigned", gamepad->deviceID);
|
||||
Controller* limeController;
|
||||
limeController = [[Controller alloc] init];
|
||||
limeController.playerIndex = i;
|
||||
|
||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void) removeGamepad:(struct Gamepad_device *)gamepad {
|
||||
_controllerNumbers &= ~(1 << gamepad->deviceID);
|
||||
Log(LOG_I, @"Unassigning controller index: %ld", (long)gamepad->deviceID);
|
||||
|
||||
// Inform the server of the updated active gamepads before removing this controller
|
||||
[self updateFinished:[_controllers objectForKey:[NSNumber numberWithInteger:gamepad->deviceID]]];
|
||||
[_controllers removeObjectForKey:[NSNumber numberWithInteger:gamepad->deviceID]];
|
||||
}
|
||||
#endif
|
||||
|
||||
+(bool) isSupportedGamepad:(GCController*) controller {
|
||||
return controller.extendedGamepad != nil || controller.gamepad != nil;
|
||||
@@ -462,38 +596,26 @@
|
||||
mask = 0x1;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
|
||||
TemporarySettings* settings = [dataMan getSettings];
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
||||
|
||||
// Even if no gamepads are present, we will always count one
|
||||
// if OSC is enabled.
|
||||
if (level != OnScreenControlsLevelOff) {
|
||||
// Even if no gamepads are present, we will always count one if OSC is enabled,
|
||||
// or it's set to auto and no keyboard or mouse is present. Absolute touch mode
|
||||
// disables the OSC.
|
||||
if (level != OnScreenControlsLevelOff && (![ControllerSupport hasKeyboardOrMouse] || level != OnScreenControlsLevelAuto) && !settings.absoluteTouchMode) {
|
||||
mask |= 0x1;
|
||||
}
|
||||
#endif
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
-(void) rumbleTimer
|
||||
-(NSUInteger) getConnectedGamepadCount
|
||||
{
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
|
||||
if (controller == nil && i == 0 && _oscEnabled) {
|
||||
// No physical controller, but we have on-screen controls
|
||||
controller = _player0osc;
|
||||
}
|
||||
if (controller == nil) {
|
||||
// No connected controller for this player
|
||||
continue;
|
||||
}
|
||||
|
||||
[self rumbleController:controller];
|
||||
}
|
||||
return _controllers.count;
|
||||
}
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig presenceDelegate:(id<InputPresenceDelegate>)delegate
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
@@ -501,24 +623,13 @@
|
||||
_controllers = [[NSMutableDictionary alloc] init];
|
||||
_controllerNumbers = 0;
|
||||
_multiController = streamConfig.multiController;
|
||||
_presenceDelegate = delegate;
|
||||
|
||||
_player0osc = [[Controller alloc] init];
|
||||
_player0osc.playerIndex = 0;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
|
||||
#else
|
||||
_oscEnabled = false;
|
||||
initGamepad(self);
|
||||
Gamepad_detectDevices();
|
||||
#endif
|
||||
|
||||
_rumbleTimer = [NSTimer scheduledTimerWithTimeInterval:0.20
|
||||
target:self
|
||||
selector:@selector(rumbleTimer)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
|
||||
Log(LOG_I, @"Multi-controller: %d", _multiController);
|
||||
@@ -527,11 +638,16 @@
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
[self assignController:controller];
|
||||
[self registerControllerCallbacks:controller];
|
||||
[self updateAutoOnScreenControlMode];
|
||||
}
|
||||
}
|
||||
|
||||
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
for (GCMouse* mouse in [GCMouse mice]) {
|
||||
[self registerMouseCallbacks:mouse];
|
||||
}
|
||||
}
|
||||
|
||||
_controllerConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Controller connected!");
|
||||
|
||||
GCController* controller = note.object;
|
||||
@@ -548,8 +664,11 @@
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
|
||||
// Notify the delegate
|
||||
[self->_presenceDelegate gamepadPresenceChanged];
|
||||
}];
|
||||
self.disconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
_controllerDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Controller disconnected!");
|
||||
|
||||
GCController* controller = note.object;
|
||||
@@ -565,6 +684,10 @@
|
||||
|
||||
// Unset the GCController on this object (in case it is the OSC, which will persist)
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
// Stop haptics on this controller
|
||||
[self cleanupControllerHaptics:limeController];
|
||||
|
||||
limeController.gamepad = nil;
|
||||
|
||||
// Inform the server of the updated active gamepads before removing this controller
|
||||
@@ -573,23 +696,91 @@
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
|
||||
// Notify the delegate
|
||||
[self->_presenceDelegate gamepadPresenceChanged];
|
||||
}];
|
||||
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
_mouseConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Mouse connected!");
|
||||
|
||||
GCMouse* mouse = note.object;
|
||||
|
||||
// Register for mouse events
|
||||
[self registerMouseCallbacks: mouse];
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
|
||||
// Notify the delegate
|
||||
[self->_presenceDelegate mousePresenceChanged];
|
||||
}];
|
||||
_mouseDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCMouseDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Mouse disconnected!");
|
||||
|
||||
GCMouse* mouse = note.object;
|
||||
|
||||
// Unregister for mouse events
|
||||
[self unregisterMouseCallbacks: mouse];
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
|
||||
// Notify the delegate
|
||||
[self->_presenceDelegate mousePresenceChanged];
|
||||
}];
|
||||
_keyboardConnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Keyboard connected!");
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
}];
|
||||
_keyboardDisconnectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCKeyboardDidDisconnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Keyboard disconnected!");
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
}];
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void) cleanup
|
||||
{
|
||||
[_rumbleTimer invalidate];
|
||||
_rumbleTimer = nil;
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
||||
[_controllers removeAllObjects];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_controllerConnectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_controllerDisconnectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_mouseConnectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_mouseDisconnectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_keyboardConnectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:_keyboardDisconnectObserver];
|
||||
|
||||
_controllerConnectObserver = nil;
|
||||
_controllerDisconnectObserver = nil;
|
||||
_mouseConnectObserver = nil;
|
||||
_mouseDisconnectObserver = nil;
|
||||
_keyboardConnectObserver = nil;
|
||||
_keyboardDisconnectObserver = nil;
|
||||
|
||||
_controllerNumbers = 0;
|
||||
|
||||
for (Controller* controller in [_controllers allValues]) {
|
||||
[self cleanupControllerHaptics:controller];
|
||||
}
|
||||
[_controllers removeAllObjects];
|
||||
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
}
|
||||
}
|
||||
|
||||
if (@available(iOS 14.0, tvOS 14.0, *)) {
|
||||
for (GCMouse* mouse in [GCMouse mice]) {
|
||||
[self unregisterMouseCallbacks:mouse];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,147 @@
|
||||
//
|
||||
// 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;
|
||||
}
|
||||
|
||||
_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;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -16,6 +16,7 @@ struct KeyEvent {
|
||||
u_char modifier;
|
||||
};
|
||||
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4));
|
||||
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,6 +11,223 @@
|
||||
|
||||
@implementation KeyboardSupport
|
||||
|
||||
+ (BOOL)sendKeyEvent:(UIKey*)key down:(BOOL)down API_AVAILABLE(ios(13.4)){
|
||||
char modifierFlags = 0;
|
||||
short keyCode = 0;
|
||||
|
||||
if (key.modifierFlags & UIKeyModifierShift) {
|
||||
modifierFlags |= MODIFIER_SHIFT;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierAlternate) {
|
||||
modifierFlags |= MODIFIER_ALT;
|
||||
}
|
||||
if (key.modifierFlags & UIKeyModifierControl) {
|
||||
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
|
||||
if (key.keyCode >= UIKeyboardHIDUsageKeyboardA &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardZ) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardA) + 0x41;
|
||||
}
|
||||
else if (key.keyCode == UIKeyboardHIDUsageKeyboard0) {
|
||||
// This key is at the beginning of the VK_ range but the end
|
||||
// of the UIKeyboardHIDUsageKeyboard range.
|
||||
keyCode = 0x30;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboard1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboard9) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboard1) + 0x31;
|
||||
}
|
||||
else if (key.keyCode == UIKeyboardHIDUsageKeypad0) {
|
||||
// This key is at the beginning of the VK_ range but the end
|
||||
// of the UIKeyboardHIDUsageKeypad range.
|
||||
keyCode = 0x60;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeypad1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeypad9) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeypad1) + 0x61;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF1 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardF12) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF1) + 0x70;
|
||||
}
|
||||
else if (key.keyCode >= UIKeyboardHIDUsageKeyboardF13 &&
|
||||
key.keyCode <= UIKeyboardHIDUsageKeyboardF24) {
|
||||
keyCode = (key.keyCode - UIKeyboardHIDUsageKeyboardF13) + 0x7C;
|
||||
}
|
||||
else {
|
||||
switch (key.keyCode) {
|
||||
case UIKeyboardHIDUsageKeyboardReturnOrEnter:
|
||||
keyCode = 0x0D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEscape:
|
||||
keyCode = 0x1B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDeleteOrBackspace:
|
||||
keyCode = 0x08;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardTab:
|
||||
keyCode = 0x09;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSpacebar:
|
||||
keyCode = 0x20;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardHyphen:
|
||||
keyCode = 0xBD;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEqualSign:
|
||||
keyCode = 0xBB;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardOpenBracket:
|
||||
keyCode = 0xDB;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCloseBracket:
|
||||
keyCode = 0xDD;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardBackslash:
|
||||
keyCode = 0xDC;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSemicolon:
|
||||
keyCode = 0xBA;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardQuote:
|
||||
keyCode = 0xDE;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardGraveAccentAndTilde:
|
||||
keyCode = 0xC0;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardComma:
|
||||
keyCode = 0xBC;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPeriod:
|
||||
keyCode = 0xBE;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardSlash:
|
||||
keyCode = 0xBF;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCapsLock:
|
||||
keyCode = 0x14;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPrintScreen:
|
||||
keyCode = 0x2A;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardScrollLock:
|
||||
keyCode = 0x91;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPause:
|
||||
keyCode = 0x13;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardInsert:
|
||||
keyCode = 0x2D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardHome:
|
||||
keyCode = 0x24;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPageUp:
|
||||
keyCode = 0x21;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDeleteForward:
|
||||
keyCode = 0x2E;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardEnd:
|
||||
keyCode = 0x23;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardPageDown:
|
||||
keyCode = 0x22;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightArrow:
|
||||
keyCode = 0x27;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftArrow:
|
||||
keyCode = 0x25;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardDownArrow:
|
||||
keyCode = 0x28;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardUpArrow:
|
||||
keyCode = 0x26;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadNumLock:
|
||||
keyCode = 0x90;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadSlash:
|
||||
keyCode = 0x6F;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadAsterisk:
|
||||
keyCode = 0x6A;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadHyphen:
|
||||
keyCode = 0x6D;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadPlus:
|
||||
keyCode = 0x6B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadEnter:
|
||||
keyCode = 0x0D; // FIXME: Is this correct?
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadPeriod:
|
||||
keyCode = 0x6E;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardNonUSBackslash:
|
||||
keyCode = 0xE2;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeypadComma:
|
||||
keyCode = 0x6C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCancel:
|
||||
keyCode = 0x03;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardClear:
|
||||
keyCode = 0x0C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardCrSelOrProps:
|
||||
keyCode = 0xF7;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardExSel:
|
||||
keyCode = 0xF8;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftGUI:
|
||||
keyCode = 0x5B;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftControl:
|
||||
keyCode = 0xA2;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftShift:
|
||||
keyCode = 0xA0;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardLeftAlt:
|
||||
keyCode = 0xA4;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightGUI:
|
||||
keyCode = 0x5C;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightControl:
|
||||
keyCode = 0xA3;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightShift:
|
||||
keyCode = 0xA1;
|
||||
break;
|
||||
case UIKeyboardHIDUsageKeyboardRightAlt:
|
||||
keyCode = 0xA5;
|
||||
break;
|
||||
default:
|
||||
NSLog(@"Unhandled HID usage: %lu", (unsigned long)key.keyCode);
|
||||
assert(0);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
LiSendKeyboardEvent(0x8000 | keyCode,
|
||||
down ? KEY_ACTION_DOWN : KEY_ACTION_UP,
|
||||
modifierFlags);
|
||||
return true;
|
||||
}
|
||||
|
||||
+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
|
||||
struct KeyEvent event;
|
||||
event.keycode = 0;
|
||||
@@ -25,10 +242,12 @@
|
||||
case UIKeyModifierControl:
|
||||
[KeyboardSupport addControlModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
[KeyboardSupport addMetaModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierAlternate:
|
||||
[KeyboardSupport addAltModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
case UIKeyModifierNumericPad:
|
||||
break;
|
||||
}
|
||||
@@ -183,6 +402,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;
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "StreamView.h"
|
||||
|
||||
@class ControllerSupport;
|
||||
|
||||
@@ -25,12 +24,12 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||
};
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport;
|
||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||
#endif
|
||||
- (OnScreenControlsLevel) getLevel;
|
||||
- (void) show;
|
||||
|
||||
@end
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "OnScreenControls.h"
|
||||
#import "StreamView.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "Controller.h"
|
||||
#include "Limelight.h"
|
||||
@@ -35,7 +36,6 @@
|
||||
CALayer* _l1Button;
|
||||
CALayer* _l2Button;
|
||||
CALayer* _l3Button;
|
||||
CALayer* _edge;
|
||||
|
||||
UITouch* _aTouch;
|
||||
UITouch* _bTouch;
|
||||
@@ -52,7 +52,6 @@
|
||||
UITouch* _l1Touch;
|
||||
UITouch* _l2Touch;
|
||||
UITouch* _l3Touch;
|
||||
UITouch* _edgeTouch;
|
||||
|
||||
NSDate* l3TouchStart;
|
||||
NSDate* r3TouchStart;
|
||||
@@ -64,10 +63,10 @@
|
||||
CGRect _controlArea;
|
||||
UIView* _view;
|
||||
OnScreenControlsLevel _level;
|
||||
BOOL _visible;
|
||||
|
||||
ControllerSupport *_controllerSupport;
|
||||
Controller *_controller;
|
||||
id<EdgeDetectionDelegate> _edgeDelegate;
|
||||
NSMutableArray* _deadTouches;
|
||||
}
|
||||
|
||||
@@ -112,12 +111,11 @@ 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 {
|
||||
self = [self init];
|
||||
_view = view;
|
||||
_controllerSupport = controllerSupport;
|
||||
_controller = [controllerSupport getOscController];
|
||||
_edgeDelegate = swipeDelegate;
|
||||
_deadTouches = [[NSMutableArray alloc] init];
|
||||
|
||||
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||||
@@ -154,16 +152,28 @@ static float L3_Y;
|
||||
_rightStickBackground = [CALayer layer];
|
||||
_leftStick = [CALayer layer];
|
||||
_rightStick = [CALayer layer];
|
||||
_edge = [CALayer layer];
|
||||
|
||||
[self setupEdgeDetection];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) show {
|
||||
_visible = YES;
|
||||
|
||||
[self updateControls];
|
||||
}
|
||||
|
||||
- (void) setLevel:(OnScreenControlsLevel)level {
|
||||
_level = level;
|
||||
[self updateControls];
|
||||
|
||||
// Only update controls if we're showing, otherwise
|
||||
// show will do it for us.
|
||||
if (_visible) {
|
||||
[self updateControls];
|
||||
}
|
||||
}
|
||||
|
||||
- (OnScreenControlsLevel) getLevel {
|
||||
return _level;
|
||||
}
|
||||
|
||||
- (void) updateControls {
|
||||
@@ -237,11 +247,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
|
||||
@@ -641,7 +646,7 @@ static float L3_Y;
|
||||
if (updated) {
|
||||
[_controllerSupport updateFinished:_controller];
|
||||
}
|
||||
return updated || buttonTouch;
|
||||
return updated || buttonTouch;
|
||||
}
|
||||
|
||||
- (BOOL)handleTouchDownEvent:touches {
|
||||
@@ -752,8 +757,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];
|
||||
@@ -837,11 +840,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];
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,204 @@
|
||||
//
|
||||
// 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;
|
||||
|
||||
#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;
|
||||
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 ([[event allTouches] count] == 2) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
} else if ([[event allTouches] count] == 1) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
self->isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
#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,19 +7,31 @@
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "OnScreenControls.h"
|
||||
#import "Moonlight-Swift.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol EdgeDetectionDelegate <NSObject>
|
||||
@protocol UserInteractionDelegate <NSObject>
|
||||
|
||||
- (void) edgeSwiped;
|
||||
- (void) userInteractionBegan;
|
||||
- (void) userInteractionEnded;
|
||||
|
||||
@end
|
||||
|
||||
@interface StreamView : OSView <UITextFieldDelegate>
|
||||
#if TARGET_OS_TV
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate>
|
||||
#else
|
||||
@interface StreamView : UIView <X1KitMouseDelegate, UITextFieldDelegate, UIPointerInteractionDelegate>
|
||||
#endif
|
||||
|
||||
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig;
|
||||
- (void) showOnScreenControls;
|
||||
- (OnScreenControlsLevel) getCurrentOscState;
|
||||
|
||||
- (void) setupStreamView;
|
||||
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
|
||||
- (void) setMouseDeltaFactors:(float)x y:(float)y;
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
+506
-178
@@ -8,244 +8,508 @@
|
||||
|
||||
#import "StreamView.h"
|
||||
#include <Limelight.h>
|
||||
#import "OnScreenControls.h"
|
||||
#import "DataManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "KeyboardSupport.h"
|
||||
#import "RelativeTouchHandler.h"
|
||||
#import "AbsoluteTouchHandler.h"
|
||||
|
||||
static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
|
||||
|
||||
@implementation StreamView {
|
||||
CGPoint touchLocation, originalLocation;
|
||||
BOOL touchMoved;
|
||||
OnScreenControls* onScreenControls;
|
||||
|
||||
UITextField* keyInputField;
|
||||
BOOL isInputingText;
|
||||
BOOL isDragging;
|
||||
NSTimer* dragTimer;
|
||||
|
||||
float xDeltaFactor;
|
||||
float yDeltaFactor;
|
||||
float screenFactor;
|
||||
float streamAspectRatio;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
// iOS 13.4 mouse support
|
||||
NSInteger lastMouseButtonMask;
|
||||
float lastMouseX;
|
||||
float lastMouseY;
|
||||
CGPoint lastScrollTranslation;
|
||||
|
||||
// Citrix X1 mouse support
|
||||
X1Mouse* x1mouse;
|
||||
double accumulatedMouseDeltaX;
|
||||
double accumulatedMouseDeltaY;
|
||||
|
||||
UIResponder* touchHandler;
|
||||
|
||||
id<UserInteractionDelegate> interactionDelegate;
|
||||
NSTimer* interactionTimer;
|
||||
BOOL hasUserInteracted;
|
||||
|
||||
NSDictionary<NSString *, NSNumber *> *dictCodes;
|
||||
}
|
||||
|
||||
- (void) setMouseDeltaFactors:(float)x y:(float)y {
|
||||
xDeltaFactor = x;
|
||||
yDeltaFactor = y;
|
||||
- (void) setupStreamView:(ControllerSupport*)controllerSupport
|
||||
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
|
||||
config:(StreamConfiguration*)streamConfig {
|
||||
self->interactionDelegate = interactionDelegate;
|
||||
self->streamAspectRatio = (float)streamConfig.width / (float)streamConfig.height;
|
||||
|
||||
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
|
||||
|
||||
keyInputField = [[UITextField alloc] initWithFrame:CGRectZero];
|
||||
[keyInputField setKeyboardType:UIKeyboardTypeDefault];
|
||||
[keyInputField setAutocorrectionType:UITextAutocorrectionTypeNo];
|
||||
[keyInputField setAutocapitalizationType:UITextAutocapitalizationTypeNone];
|
||||
[keyInputField setSpellCheckingType:UITextSpellCheckingTypeNo];
|
||||
[self addSubview:keyInputField];
|
||||
|
||||
screenFactor = [[UIScreen mainScreen] scale];
|
||||
}
|
||||
|
||||
- (void) setupStreamView {
|
||||
#if TARGET_OS_TV
|
||||
remotePressRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonPressed:)];
|
||||
remotePressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
// tvOS requires RelativeTouchHandler to manage Apple Remote input
|
||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
||||
#else
|
||||
// iOS uses RelativeTouchHandler or AbsoluteTouchHandler depending on user preference
|
||||
if (settings.absoluteTouchMode) {
|
||||
self->touchHandler = [[AbsoluteTouchHandler alloc] initWithView:self];
|
||||
}
|
||||
else {
|
||||
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
|
||||
}
|
||||
|
||||
remoteLongPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(remoteButtonLongPressed:)];
|
||||
remoteLongPressRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)];
|
||||
|
||||
[self addGestureRecognizer:remotePressRecognizer];
|
||||
[self addGestureRecognizer:remoteLongPressRecognizer];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate {
|
||||
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport swipeDelegate:swipeDelegate];
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
|
||||
|
||||
if (level == OnScreenControlsLevelAuto) {
|
||||
onScreenControls = [[OnScreenControls alloc] initWithView:self controllerSup:controllerSupport];
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[settings.onscreenControls integerValue];
|
||||
if (settings.absoluteTouchMode) {
|
||||
Log(LOG_I, @"On-screen controls disabled in absolute touch mode");
|
||||
[onScreenControls setLevel:OnScreenControlsLevelOff];
|
||||
}
|
||||
else if (level == OnScreenControlsLevelAuto) {
|
||||
[controllerSupport initAutoOnScreenControlMode:onScreenControls];
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
||||
[onScreenControls setLevel:level];
|
||||
}
|
||||
|
||||
// It would be nice to just use GCMouse on iOS 14+ and the older API on iOS 13
|
||||
// but unfortunately that isn't possible today. GCMouse doesn't recognize many
|
||||
// mice correctly, but UIKit does. We will register for both and ignore UIKit
|
||||
// events if a GCMouse is connected.
|
||||
if (@available(iOS 13.4, *)) {
|
||||
[self addInteraction:[[UIPointerInteraction alloc] initWithDelegate:self]];
|
||||
|
||||
UIPanGestureRecognizer *discreteMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedDiscrete:)];
|
||||
discreteMouseWheelRecognizer.maximumNumberOfTouches = 0;
|
||||
discreteMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskDiscrete;
|
||||
discreteMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
||||
[self addGestureRecognizer:discreteMouseWheelRecognizer];
|
||||
|
||||
UIPanGestureRecognizer *continuousMouseWheelRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(mouseWheelMovedContinuous:)];
|
||||
continuousMouseWheelRecognizer.maximumNumberOfTouches = 0;
|
||||
continuousMouseWheelRecognizer.allowedScrollTypesMask = UIScrollTypeMaskContinuous;
|
||||
continuousMouseWheelRecognizer.allowedTouchTypes = @[@(UITouchTypeIndirectPointer)];
|
||||
[self addGestureRecognizer:continuousMouseWheelRecognizer];
|
||||
}
|
||||
#endif
|
||||
|
||||
x1mouse = [[X1Mouse alloc] init];
|
||||
x1mouse.delegate = self;
|
||||
|
||||
if (settings.btMouseSupport) {
|
||||
[x1mouse start];
|
||||
}
|
||||
|
||||
// This is critical to ensure keyboard events are delivered to this
|
||||
// StreamView and not our parent UIView, especially on tvOS.
|
||||
[self becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
// Movements of greater than 10 pixels are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10;
|
||||
- (void)startInteractionTimer {
|
||||
// Restart user interaction tracking
|
||||
hasUserInteracted = NO;
|
||||
|
||||
BOOL timerAlreadyRunning = interactionTimer != nil;
|
||||
|
||||
// Start/restart the timer
|
||||
[interactionTimer invalidate];
|
||||
interactionTimer = [NSTimer scheduledTimerWithTimeInterval:2.0
|
||||
target:self
|
||||
selector:@selector(interactionTimerExpired:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
|
||||
// Notify the delegate if this was a new user interaction
|
||||
if (!timerAlreadyRunning) {
|
||||
[interactionDelegate userInteractionBegan];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)interactionTimerExpired:(NSTimer *)timer {
|
||||
if (!hasUserInteracted) {
|
||||
// User has finished touching the screen
|
||||
interactionTimer = nil;
|
||||
[interactionDelegate userInteractionEnded];
|
||||
}
|
||||
else {
|
||||
// User is still touching the screen. Restart the timer.
|
||||
[self startInteractionTimer];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) showOnScreenControls {
|
||||
#if !TARGET_OS_TV
|
||||
[onScreenControls show];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (OnScreenControlsLevel) getCurrentOscState {
|
||||
if (onScreenControls == nil) {
|
||||
return OnScreenControlsLevelOff;
|
||||
}
|
||||
else {
|
||||
return [onScreenControls getLevel];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_PRESS
|
||||
forTouches:touches
|
||||
withEvent:event]) {
|
||||
// If it's a mouse event, we're done
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_D, @"Touch down");
|
||||
|
||||
// Notify of user interaction and start expiration timer
|
||||
[self startInteractionTimer];
|
||||
|
||||
if (![onScreenControls handleTouchDownEvent:touches]) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
originalLocation = touchLocation = [touch locationInView:self];
|
||||
touchMoved = false;
|
||||
if ([[event allTouches] count] == 1 && !isDragging) {
|
||||
dragTimer = [NSTimer scheduledTimerWithTimeInterval:0.650
|
||||
target:self
|
||||
selector:@selector(onDragStart:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
if ([[event allTouches] count] == 3) {
|
||||
if (isInputingText) {
|
||||
Log(LOG_D, @"Closing the keyboard");
|
||||
[keyInputField resignFirstResponder];
|
||||
isInputingText = false;
|
||||
} else {
|
||||
Log(LOG_D, @"Opening the keyboard");
|
||||
// Prepare the textbox used to capture keyboard events.
|
||||
keyInputField.delegate = self;
|
||||
keyInputField.text = @"0";
|
||||
[keyInputField becomeFirstResponder];
|
||||
[keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
|
||||
|
||||
// Undo causes issues for our state management, so turn it off
|
||||
[keyInputField.undoManager disableUndoRegistration];
|
||||
|
||||
isInputingText = true;
|
||||
}
|
||||
}
|
||||
else {
|
||||
[touchHandler touchesBegan:touches withEvent:event];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)onDragStart:(NSTimer*)timer {
|
||||
if (!touchMoved && !isDragging){
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
- (BOOL)handleMouseButtonEvent:(int)buttonAction forTouches:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.4, *)) {
|
||||
UITouch* touch = [touches anyObject];
|
||||
if (touch.type == UITouchTypeIndirectPointer) {
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if ([GCMouse current] != nil) {
|
||||
// We'll handle this with GCMouse. Do nothing here.
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
UIEventButtonMask normalizedButtonMask;
|
||||
|
||||
// iOS 14 includes the released button in the buttonMask for the release
|
||||
// event, while iOS 13 does not. Normalize that behavior here.
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if (buttonAction == BUTTON_ACTION_RELEASE) {
|
||||
normalizedButtonMask = lastMouseButtonMask & ~event.buttonMask;
|
||||
}
|
||||
else {
|
||||
normalizedButtonMask = event.buttonMask;
|
||||
}
|
||||
}
|
||||
else {
|
||||
normalizedButtonMask = event.buttonMask;
|
||||
}
|
||||
|
||||
UIEventButtonMask changedButtons = lastMouseButtonMask ^ normalizedButtonMask;
|
||||
|
||||
for (int i = BUTTON_LEFT; i <= BUTTON_X2; i++) {
|
||||
UIEventButtonMask buttonFlag;
|
||||
|
||||
switch (i) {
|
||||
// Right and Middle are reversed from what iOS uses
|
||||
case BUTTON_RIGHT:
|
||||
buttonFlag = UIEventButtonMaskForButtonNumber(2);
|
||||
break;
|
||||
case BUTTON_MIDDLE:
|
||||
buttonFlag = UIEventButtonMaskForButtonNumber(3);
|
||||
break;
|
||||
|
||||
default:
|
||||
buttonFlag = UIEventButtonMaskForButtonNumber(i);
|
||||
break;
|
||||
}
|
||||
|
||||
if (changedButtons & buttonFlag) {
|
||||
LiSendMouseButtonEvent(buttonAction, i);
|
||||
}
|
||||
}
|
||||
|
||||
lastMouseButtonMask = normalizedButtonMask;
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
CGPoint currentLocation = [touch locationInView:self];
|
||||
|
||||
if (touchLocation.x != currentLocation.x ||
|
||||
touchLocation.y != currentLocation.y)
|
||||
{
|
||||
int deltaX = currentLocation.x - touchLocation.x;
|
||||
int deltaY = currentLocation.y - touchLocation.y;
|
||||
|
||||
deltaX *= xDeltaFactor * screenFactor;
|
||||
deltaY *= yDeltaFactor * screenFactor;
|
||||
|
||||
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;
|
||||
}
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 13.4, *)) {
|
||||
UITouch *touch = [touches anyObject];
|
||||
if (touch.type == UITouchTypeIndirectPointer) {
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if ([GCMouse current] != nil) {
|
||||
// We'll handle this with GCMouse. Do nothing here.
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else if ([[event allTouches] count] == 2) {
|
||||
CGPoint firstLocation = [[[[event allTouches] allObjects] objectAtIndex:0] locationInView:self];
|
||||
CGPoint secondLocation = [[[[event allTouches] allObjects] objectAtIndex:1] locationInView:self];
|
||||
|
||||
CGPoint avgLocation = CGPointMake((firstLocation.x + secondLocation.x) / 2, (firstLocation.y + secondLocation.y) / 2);
|
||||
if (touchLocation.y != avgLocation.y) {
|
||||
LiSendScrollEvent(avgLocation.y - touchLocation.y);
|
||||
}
|
||||
// We must handle this event to properly support
|
||||
// drags while the middle, X1, or X2 mouse buttons are
|
||||
// held down. For some reason, left and right buttons
|
||||
// don't require this, but we do it anyway for them too.
|
||||
// Cursor movement without a button held down is handled
|
||||
// in pointerInteraction:regionForRequest:defaultRegion.
|
||||
[self updateCursorLocation:[touch locationInView:self] isMouse:YES];
|
||||
return;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
hasUserInteracted = YES;
|
||||
|
||||
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
||||
[touchHandler touchesMoved:touches withEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
- (void)pressesBegan:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
|
||||
BOOL handled = NO;
|
||||
|
||||
if (@available(iOS 13.4, tvOS 13.4, *)) {
|
||||
for (UIPress* press in presses) {
|
||||
// For now, we'll treated it as handled if we handle at least one of the
|
||||
// UIPress events inside the set.
|
||||
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:YES]) {
|
||||
// This will prevent the legacy UITextField from receiving the event
|
||||
handled = YES;
|
||||
}
|
||||
|
||||
touchLocation = avgLocation;
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
[super pressesBegan:presses withEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)pressesEnded:(NSSet<UIPress *> *)presses withEvent:(UIPressesEvent *)event {
|
||||
BOOL handled = NO;
|
||||
|
||||
if (@available(iOS 13.4, tvOS 13.4, *)) {
|
||||
for (UIPress* press in presses) {
|
||||
// For now, we'll treated it as handled if we handle at least one of the
|
||||
// UIPress events inside the set.
|
||||
if (press.key != nil && [KeyboardSupport sendKeyEvent:press.key down:NO]) {
|
||||
// This will prevent the legacy UITextField from receiving the event
|
||||
handled = YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!handled) {
|
||||
[super pressesEnded:presses withEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if ([self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
||||
forTouches:touches
|
||||
withEvent:event]) {
|
||||
// If it's a mouse event, we're done
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_D, @"Touch up");
|
||||
|
||||
hasUserInteracted = YES;
|
||||
|
||||
if (![onScreenControls handleTouchUpEvent:touches]) {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
} else if (!touchMoved) {
|
||||
if ([[event allTouches] count] == 3) {
|
||||
if (isInputingText) {
|
||||
Log(LOG_D, @"Closing the keyboard");
|
||||
[_keyInputField resignFirstResponder];
|
||||
isInputingText = false;
|
||||
} else {
|
||||
Log(LOG_D, @"Opening the keyboard");
|
||||
// Prepare the textbox used to capture keyboard events.
|
||||
_keyInputField.delegate = self;
|
||||
_keyInputField.text = @"0";
|
||||
[_keyInputField becomeFirstResponder];
|
||||
[_keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
|
||||
|
||||
// Undo causes issues for our state management, so turn it off
|
||||
[_keyInputField.undoManager disableUndoRegistration];
|
||||
|
||||
isInputingText = true;
|
||||
}
|
||||
} else if ([[event allTouches] count] == 2) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
} else if ([[event allTouches] count] == 1) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
self->isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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:self];
|
||||
|
||||
// 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;
|
||||
}
|
||||
[touchHandler touchesEnded:touches withEvent:event];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
[touchHandler touchesCancelled:touches withEvent:event];
|
||||
[self handleMouseButtonEvent:BUTTON_ACTION_RELEASE
|
||||
forTouches:touches
|
||||
withEvent:event];
|
||||
}
|
||||
|
||||
#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");
|
||||
#if !TARGET_OS_TV
|
||||
- (void) updateCursorLocation:(CGPoint)location isMouse:(BOOL)isMouse {
|
||||
// These are now relative to the StreamView, however we need to scale them
|
||||
// further to make them relative to the actual video portion.
|
||||
float x = location.x - self.bounds.origin.x;
|
||||
float y = location.y - self.bounds.origin.y;
|
||||
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
// For some reason, we don't seem to always get to the bounds of the window
|
||||
// so we'll subtract 1 pixel if we're to the left/below of the origin and
|
||||
// and add 1 pixel if we're to the right/above. It should be imperceptible
|
||||
// to the user but it will allow activation of gestures that require contact
|
||||
// with the edge of the screen (like Aero Snap).
|
||||
if (x < self.bounds.size.width / 2) {
|
||||
x--;
|
||||
}
|
||||
else {
|
||||
x++;
|
||||
}
|
||||
if (y < self.bounds.size.height / 2) {
|
||||
y--;
|
||||
}
|
||||
else {
|
||||
y++;
|
||||
}
|
||||
|
||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
||||
CGSize videoSize;
|
||||
CGPoint videoOrigin;
|
||||
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
|
||||
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
|
||||
} else {
|
||||
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
|
||||
}
|
||||
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
|
||||
self.bounds.size.height / 2 - videoSize.height / 2);
|
||||
|
||||
// Confine the cursor to the video region. We don't just discard events outside
|
||||
// the region because we won't always get one exactly when the mouse leaves the region.
|
||||
x = MIN(MAX(x, videoOrigin.x), videoOrigin.x + videoSize.width);
|
||||
y = MIN(MAX(y, videoOrigin.y), videoOrigin.y + videoSize.height);
|
||||
|
||||
// Send the mouse position relative to the video region if it has changed
|
||||
// if we're receiving coordinates from a real mouse.
|
||||
//
|
||||
// NB: It is important for functionality (not just optimization) to only
|
||||
// send it if the value has changed. We will receive one of these events
|
||||
// any time the user presses a modifier key, which can result in errant
|
||||
// mouse motion when using a Citrix X1 mouse.
|
||||
if (x != lastMouseX || y != lastMouseY || !isMouse) {
|
||||
if (lastMouseX != 0 || lastMouseY != 0 || !isMouse) {
|
||||
LiSendMousePositionEvent(x - videoOrigin.x, y - videoOrigin.y,
|
||||
videoSize.width, videoSize.height);
|
||||
}
|
||||
|
||||
if (isMouse) {
|
||||
lastMouseX = x;
|
||||
lastMouseY = y;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (UIPointerRegion *)pointerInteraction:(UIPointerInteraction *)interaction
|
||||
regionForRequest:(UIPointerRegionRequest *)request
|
||||
defaultRegion:(UIPointerRegion *)defaultRegion API_AVAILABLE(ios(13.4)) {
|
||||
if (@available(iOS 14.0, *)) {
|
||||
if ([GCMouse current] != nil) {
|
||||
// We'll handle this with GCMouse. Do nothing here.
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// This logic mimics what iOS does with AVLayerVideoGravityResizeAspect
|
||||
CGSize videoSize;
|
||||
CGPoint videoOrigin;
|
||||
if (self.bounds.size.width > self.bounds.size.height * streamAspectRatio) {
|
||||
videoSize = CGSizeMake(self.bounds.size.height * streamAspectRatio, self.bounds.size.height);
|
||||
} else {
|
||||
videoSize = CGSizeMake(self.bounds.size.width, self.bounds.size.width / streamAspectRatio);
|
||||
}
|
||||
videoOrigin = CGPointMake(self.bounds.size.width / 2 - videoSize.width / 2,
|
||||
self.bounds.size.height / 2 - videoSize.height / 2);
|
||||
|
||||
// Move the cursor on the host if no buttons are pressed.
|
||||
// Motion with buttons pressed in handled in touchesMoved:
|
||||
if (lastMouseButtonMask == 0) {
|
||||
[self updateCursorLocation:request.location isMouse:YES];
|
||||
}
|
||||
|
||||
// The pointer interaction should cover the video region only
|
||||
return [UIPointerRegion regionWithRect:CGRectMake(videoOrigin.x, videoOrigin.y, videoSize.width, videoSize.height) identifier:nil];
|
||||
}
|
||||
|
||||
- (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region API_AVAILABLE(ios(13.4)) {
|
||||
// Always hide the mouse cursor over our stream view
|
||||
return [UIPointerStyle hiddenPointerStyle];
|
||||
}
|
||||
|
||||
- (void)mouseWheelMovedContinuous:(UIPanGestureRecognizer *)gesture {
|
||||
switch (gesture.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged:
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateEnded:
|
||||
default:
|
||||
// Ignore recognition failure and other states
|
||||
lastScrollTranslation = CGPointMake(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
CGPoint currentScrollTranslation = [gesture translationInView:self];
|
||||
short translationDeltaY = ((currentScrollTranslation.y - lastScrollTranslation.y) / self.bounds.size.height) * 120; // WHEEL_DELTA
|
||||
if (translationDeltaY != 0) {
|
||||
LiSendHighResScrollEvent(translationDeltaY * 20);
|
||||
lastScrollTranslation = currentScrollTranslation;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseWheelMovedDiscrete:(UIPanGestureRecognizer *)gesture {
|
||||
switch (gesture.state) {
|
||||
case UIGestureRecognizerStateBegan:
|
||||
case UIGestureRecognizerStateChanged:
|
||||
break;
|
||||
|
||||
case UIGestureRecognizerStateEnded:
|
||||
default:
|
||||
// Ignore recognition failure and other states
|
||||
lastScrollTranslation = CGPointMake(0, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Using velocityInView is 0 for discrete scroll events
|
||||
// when scrolling very slowly, but translationInView does work.
|
||||
CGPoint currentScrollTranslation = [gesture translationInView:self];
|
||||
short translationDeltaY = currentScrollTranslation.y - lastScrollTranslation.y;
|
||||
if (translationDeltaY != 0) {
|
||||
LiSendScrollEvent(translationDeltaY > 0 ? 1 : -1);
|
||||
}
|
||||
|
||||
lastScrollTranslation = currentScrollTranslation;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer {
|
||||
// Disable all gesture recognizers to prevent them from eating our touches.
|
||||
// This can happen on iOS 13 where the 3 finger tap gesture is taken over for
|
||||
// displaying custom edit controls.
|
||||
return NO;
|
||||
if (@available(iOS 13.0, *)) {
|
||||
// Disable the 3 finger tap gestures that trigger the copy/paste/undo toolbar on iOS 13+
|
||||
return gestureRecognizer.name == nil || ![gestureRecognizer.name hasPrefix:@"kbProductivity."];
|
||||
}
|
||||
else {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
@@ -266,13 +530,24 @@
|
||||
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
|
||||
} else {
|
||||
// Character 0 will be our known sentinel value
|
||||
|
||||
// Check if any characters exist which can't be represented in a basic key event
|
||||
for (int i = 1; i < [inputText length]; i++) {
|
||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
||||
if (event.keycode == 0) {
|
||||
// If we don't know the code, don't send anything.
|
||||
Log(LOG_W, @"Unknown key code: [%c]", [inputText characterAtIndex:i]);
|
||||
continue;
|
||||
// We found an unknown key, so send the entire string as UTF-8
|
||||
const char* utf8String = [inputText UTF8String];
|
||||
|
||||
// Skip the first character which is our sentinel
|
||||
LiSendUtf8TextEvent(utf8String + 1, (int)strlen(utf8String) - 1);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find any unknown characters, so send them all as basic key events
|
||||
for (int i = 1; i < [inputText length]; i++) {
|
||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
||||
assert(event.keycode != 0);
|
||||
[self sendLowLevelEvent:event];
|
||||
}
|
||||
}
|
||||
@@ -360,4 +635,57 @@
|
||||
return commands;
|
||||
}
|
||||
|
||||
- (void)connectedStateDidChangeWithIdentifier:(NSUUID * _Nonnull)identifier isConnected:(BOOL)isConnected {
|
||||
NSLog(@"Citrix X1 mouse state change: %@ -> %s",
|
||||
identifier, isConnected ? "connected" : "disconnected");
|
||||
}
|
||||
|
||||
- (void)mouseDidMoveWithIdentifier:(NSUUID * _Nonnull)identifier deltaX:(int16_t)deltaX deltaY:(int16_t)deltaY {
|
||||
accumulatedMouseDeltaX += deltaX / X1_MOUSE_SPEED_DIVISOR;
|
||||
accumulatedMouseDeltaY += deltaY / X1_MOUSE_SPEED_DIVISOR;
|
||||
|
||||
short shortX = (short)accumulatedMouseDeltaX;
|
||||
short shortY = (short)accumulatedMouseDeltaY;
|
||||
|
||||
if (shortX == 0 && shortY == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
LiSendMouseMoveEvent(shortX, shortY);
|
||||
|
||||
accumulatedMouseDeltaX -= shortX;
|
||||
accumulatedMouseDeltaY -= shortY;
|
||||
}
|
||||
|
||||
- (int) buttonFromX1ButtonCode:(enum X1MouseButton)button {
|
||||
switch (button) {
|
||||
case X1MouseButtonLeft:
|
||||
return BUTTON_LEFT;
|
||||
case X1MouseButtonRight:
|
||||
return BUTTON_RIGHT;
|
||||
case X1MouseButtonMiddle:
|
||||
return BUTTON_MIDDLE;
|
||||
default:
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
- (void)mouseDownWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, [self buttonFromX1ButtonCode:button]);
|
||||
}
|
||||
|
||||
- (void)mouseUpWithIdentifier:(NSUUID * _Nonnull)identifier button:(enum X1MouseButton)button {
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, [self buttonFromX1ButtonCode:button]);
|
||||
}
|
||||
|
||||
- (void)wheelDidScrollWithIdentifier:(NSUUID * _Nonnull)identifier deltaZ:(int8_t)deltaZ {
|
||||
LiSendScrollEvent(deltaZ);
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
- (BOOL)isMultipleTouchEnabled {
|
||||
return YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDuration</key>
|
||||
<true/>
|
||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -37,6 +39,8 @@
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
<key>ITSAppUsesNonExemptEncryption</key>
|
||||
<false/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -46,6 +50,18 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<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>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -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.4.xcdatamodel</string>
|
||||
<string>Moonlight v1.6.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="16119" systemVersion="19E266" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="210"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="238"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<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"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="ipv6Address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="absoluteTouchMode" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="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="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="268"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -16,13 +16,8 @@
|
||||
self.statusMessage = @"App asset has no status message";
|
||||
self.statusCode = -1;
|
||||
}
|
||||
- (OSImage*) getImage {
|
||||
#if TARGET_OS_IPHONE
|
||||
OSImage* appImage = [[OSImage alloc] initWithData:self.data];
|
||||
#else
|
||||
OSImage* appImage = nil;
|
||||
#endif
|
||||
return appImage;
|
||||
- (UIImage*) getImage {
|
||||
return [[UIImage alloc] initWithData:self.data];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -24,16 +24,12 @@ static const int MAX_ATTEMPTS = 5;
|
||||
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if (appAssetResp.data != nil) {
|
||||
NSString* boxArtPath = [AppAssetManager boxArtPathForApp:self.app];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[boxArtPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
[appAssetResp.data writeToFile:boxArtPath atomically:NO];
|
||||
break;
|
||||
}
|
||||
#else
|
||||
|
||||
#endif
|
||||
|
||||
if (![self isCancelled]) {
|
||||
[NSThread sleepForTimeInterval:RETRY_DELAY];
|
||||
|
||||
@@ -44,7 +44,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
|
||||
|
||||
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
|
||||
if (statusStr != NULL) {
|
||||
int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
|
||||
int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
|
||||
xmlFree(statusStr);
|
||||
self.statusCode = status;
|
||||
}
|
||||
@@ -113,6 +113,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
|
||||
|
||||
xmlFreeDoc(docPtr);
|
||||
|
||||
#ifdef ENABLE_APP_STORE_RESTRICTIONS
|
||||
// APP STORE REVIEW COMPLIANCE
|
||||
//
|
||||
// Remove default Steam entry from the app list to comply with Apple App Store Guideline 4.2.7d:
|
||||
@@ -146,6 +147,7 @@ static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
|
||||
[_appList removeObject:manuallyAddedSteamApp];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSSet*) getAppList {
|
||||
|
||||
@@ -18,11 +18,11 @@
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
|
||||
|
||||
// Try up to 5 times to get the app list
|
||||
AppListResponse* appListResp;
|
||||
AppListResponse* appListResp = nil;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
appListResp = [[AppListResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appListResp withUrlRequest:[hMan newAppListRequest]]];
|
||||
if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
||||
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
||||
Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
|
||||
|
||||
// Wait for one second then retry
|
||||
@@ -30,10 +30,11 @@
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"App list successfully retreived - took %d tries", i);
|
||||
return appListResp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
|
||||
return appListResp;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
- (void) startDiscovery;
|
||||
- (void) stopDiscovery;
|
||||
- (void) stopDiscoveryBlocking;
|
||||
- (void) resetDiscoveryState;
|
||||
- (BOOL) addHostToDiscovery:(TemporaryHost*)host;
|
||||
- (void) removeHostFromDiscovery:(TemporaryHost*)host;
|
||||
- (void) pauseDiscoveryForHost:(TemporaryHost *)host;
|
||||
- (void) resumeDiscoveryForHost:(TemporaryHost *)host;
|
||||
- (void) discoverHost:(NSString*)hostAddress withCallback:(void (^)(TemporaryHost*, NSString*))callback;
|
||||
|
||||
@end
|
||||
|
||||
@@ -15,8 +15,14 @@
|
||||
#import "ServerInfoResponse.h"
|
||||
#import "IdManager.h"
|
||||
|
||||
#include <Limelight.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <sys/socket.h>
|
||||
#include <netdb.h>
|
||||
|
||||
@implementation DiscoveryManager {
|
||||
NSMutableArray* _hostQueue;
|
||||
NSMutableSet* _pausedHosts;
|
||||
id<DiscoveryCallback> _callback;
|
||||
MDNSManager* _mdnsMan;
|
||||
NSOperationQueue* _opQueue;
|
||||
@@ -33,6 +39,7 @@
|
||||
_callback = callback;
|
||||
shouldDiscover = NO;
|
||||
_hostQueue = [NSMutableArray array];
|
||||
_pausedHosts = [NSMutableSet set];
|
||||
for (TemporaryHost* host in hosts)
|
||||
{
|
||||
[self addHostToDiscovery:host];
|
||||
@@ -47,10 +54,88 @@
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId serverCert:nil];
|
||||
+ (BOOL) isAddressLAN:(in_addr_t)addr {
|
||||
addr = htonl(addr);
|
||||
|
||||
// 10.0.0.0/8
|
||||
if ((addr & 0xFF000000) == 0x0A000000) {
|
||||
return YES;
|
||||
}
|
||||
// 172.16.0.0/12
|
||||
else if ((addr & 0xFFF00000) == 0xAC100000) {
|
||||
return YES;
|
||||
}
|
||||
// 192.168.0.0/16
|
||||
else if ((addr & 0xFFFF0000) == 0xC0A80000) {
|
||||
return YES;
|
||||
}
|
||||
// 169.254.0.0/16
|
||||
else if ((addr & 0xFFFF0000) == 0xA9FE0000) {
|
||||
return YES;
|
||||
}
|
||||
// 100.64.0.0/10 - RFC6598 official CGN address (shouldn't see this in a LAN)
|
||||
else if ((addr & 0xFFC00000) == 0x64400000) {
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
// This ensures that only RFC 1918 IPv4 addresses can be passed to
|
||||
// the Add PC dialog. This is required to comply with Apple App Store
|
||||
// Guideline 4.2.7a.
|
||||
+ (BOOL) isProhibitedAddress:(NSString*)address {
|
||||
#ifdef ENABLE_APP_STORE_RESTRICTIONS
|
||||
struct addrinfo hints;
|
||||
struct addrinfo* result;
|
||||
int err;
|
||||
|
||||
// We're explicitly using AF_INET here because we don't want to
|
||||
// ever receive a synthesized IPv6 address here, even on NAT64.
|
||||
// IPv6 addresses are not restricted here because we cannot easily
|
||||
// tell whether they are local or not.
|
||||
memset(&hints, 0, sizeof(hints));
|
||||
hints.ai_family = AF_INET;
|
||||
err = getaddrinfo([address UTF8String], NULL, &hints, &result);
|
||||
if (err != 0 || result == NULL) {
|
||||
Log(LOG_W, @"getaddrinfo(%@) failed: %d", address, err);
|
||||
return NO;
|
||||
}
|
||||
|
||||
if (result->ai_family != AF_INET) {
|
||||
// This should never happen due to our hints
|
||||
assert(result->ai_family == AF_INET);
|
||||
Log(LOG_W, @"Unexpected address family: %d", result->ai_family);
|
||||
freeaddrinfo(result);
|
||||
return NO;
|
||||
}
|
||||
|
||||
BOOL ret = ![DiscoveryManager isAddressLAN:((struct sockaddr_in*)result->ai_addr)->sin_addr.s_addr];
|
||||
freeaddrinfo(result);
|
||||
|
||||
return ret;
|
||||
#else
|
||||
return NO;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (ServerInfoResponse*) getServerInfoResponseForAddress:(NSString*)address {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:address uniqueId:_uniqueId serverCert:nil];
|
||||
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
return serverInfoResponse;
|
||||
}
|
||||
|
||||
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
|
||||
BOOL prohibitedAddress = [DiscoveryManager isProhibitedAddress:hostAddress];
|
||||
NSString* prohibitedAddressMessage = [NSString stringWithFormat: @"Moonlight only supports adding PCs on your local network on %s.",
|
||||
#if TARGET_OS_TV
|
||||
"tvOS"
|
||||
#else
|
||||
"iOS"
|
||||
#endif
|
||||
];
|
||||
ServerInfoResponse* serverInfoResponse = [self getServerInfoResponseForAddress:hostAddress];
|
||||
|
||||
TemporaryHost* host = nil;
|
||||
if ([serverInfoResponse isStatusOk]) {
|
||||
@@ -58,15 +143,74 @@
|
||||
host.activeAddress = host.address = hostAddress;
|
||||
host.state = StateOnline;
|
||||
[serverInfoResponse populateHost:host];
|
||||
|
||||
// Check if this is a new PC
|
||||
if (![self getHostInDiscovery:host.uuid]) {
|
||||
// Enforce LAN restriction for App Store Guideline 4.2.7a
|
||||
if ([DiscoveryManager isProhibitedAddress:hostAddress]) {
|
||||
// We have a prohibited address. This might be because the user specified their WAN address
|
||||
// instead of their LAN address. If that's the case, we'll try their LAN address and if we
|
||||
// can reach it through that address, we'll allow it.
|
||||
ServerInfoResponse* lanInfo = [self getServerInfoResponseForAddress:host.localAddress];
|
||||
if ([lanInfo isStatusOk]) {
|
||||
TemporaryHost* lanHost = [[TemporaryHost alloc] init];
|
||||
[lanInfo populateHost:lanHost];
|
||||
|
||||
if (![lanHost.uuid isEqualToString:host.uuid]) {
|
||||
// This is a different host, so it's prohibited
|
||||
prohibitedAddress = YES;
|
||||
}
|
||||
else {
|
||||
// This is the same host that is reachable on the LAN
|
||||
prohibitedAddress = NO;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// LAN request failed, so it's a prohibited address
|
||||
prohibitedAddress = YES;
|
||||
}
|
||||
}
|
||||
else {
|
||||
// It's an RFC 1918 IPv4 address or IPv6 address which counts as LAN
|
||||
prohibitedAddress = NO;
|
||||
}
|
||||
|
||||
if (prohibitedAddress) {
|
||||
callback(nil, prohibitedAddressMessage);
|
||||
return;
|
||||
}
|
||||
else if ([DiscoveryManager isAddressLAN:inet_addr([hostAddress UTF8String])]) {
|
||||
// Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
|
||||
// gateway's external address rather than the external address of the LAN.
|
||||
if (![Utils isActiveNetworkVPN]) {
|
||||
// This host was discovered over a permissible LAN address, so we can update our
|
||||
// external address for this host.
|
||||
struct in_addr wanAddr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
|
||||
if (err == 0) {
|
||||
char addrStr[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
|
||||
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (![self addHostToDiscovery:host]) {
|
||||
callback(nil, @"Host information updated");
|
||||
} else {
|
||||
callback(host, nil);
|
||||
}
|
||||
} else {
|
||||
} else if (!prohibitedAddress) {
|
||||
callback(nil, @"Could not connect to host. Ensure GameStream is enabled in GeForce Experience on your PC.");
|
||||
} else {
|
||||
callback(nil, prohibitedAddressMessage);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
- (void) resetDiscoveryState {
|
||||
// Allow us to rediscover hosts that were already found before
|
||||
[_mdnsMan forgetHosts];
|
||||
}
|
||||
|
||||
- (void) startDiscovery {
|
||||
@@ -77,8 +221,13 @@
|
||||
Log(LOG_I, @"Starting discovery");
|
||||
shouldDiscover = YES;
|
||||
[_mdnsMan searchForHosts];
|
||||
for (TemporaryHost* host in _hostQueue) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
|
||||
@synchronized (_hostQueue) {
|
||||
for (TemporaryHost* host in _hostQueue) {
|
||||
if (![_pausedHosts containsObject:host]) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,21 +290,53 @@
|
||||
return NO;
|
||||
}
|
||||
else {
|
||||
[_hostQueue addObject:host];
|
||||
if (shouldDiscover) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
@synchronized (_hostQueue) {
|
||||
[_hostQueue addObject:host];
|
||||
if (shouldDiscover) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) removeHostFromDiscovery:(TemporaryHost *)host {
|
||||
for (DiscoveryWorker* worker in [_opQueue operations]) {
|
||||
if ([worker getHost] == host) {
|
||||
[worker cancel];
|
||||
@synchronized (_hostQueue) {
|
||||
for (DiscoveryWorker* worker in [_opQueue operations]) {
|
||||
if ([worker getHost] == host) {
|
||||
[worker cancel];
|
||||
}
|
||||
}
|
||||
|
||||
[_hostQueue removeObject:host];
|
||||
[_pausedHosts removeObject:host];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) pauseDiscoveryForHost:(TemporaryHost *)host {
|
||||
@synchronized (_hostQueue) {
|
||||
// Stop any worker for the host
|
||||
for (DiscoveryWorker* worker in [_opQueue operations]) {
|
||||
if ([worker getHost] == host) {
|
||||
[worker cancel];
|
||||
}
|
||||
}
|
||||
|
||||
// Add it to the paused hosts list
|
||||
[_pausedHosts addObject:host];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) resumeDiscoveryForHost:(TemporaryHost *)host {
|
||||
@synchronized (_hostQueue) {
|
||||
// Remove it from the paused hosts list
|
||||
[_pausedHosts removeObject:host];
|
||||
|
||||
// Start discovery again
|
||||
if (shouldDiscover) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
}
|
||||
}
|
||||
[_hostQueue removeObject:host];
|
||||
}
|
||||
|
||||
// Override from MDNSCallback - called in a worker thread
|
||||
@@ -167,16 +348,20 @@
|
||||
[worker discoverHost];
|
||||
if ([self addHostToDiscovery:host]) {
|
||||
Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
|
||||
[self->_callback updateAllHosts:self->_hostQueue];
|
||||
@synchronized (_hostQueue) {
|
||||
[_callback updateAllHosts:_hostQueue];
|
||||
}
|
||||
} else {
|
||||
Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
|
||||
}
|
||||
}
|
||||
|
||||
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
|
||||
for (TemporaryHost* discoveredHost in _hostQueue) {
|
||||
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
|
||||
return discoveredHost;
|
||||
@synchronized (_hostQueue) {
|
||||
for (TemporaryHost* discoveredHost in _hostQueue) {
|
||||
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
|
||||
return discoveredHost;
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
|
||||
@@ -89,8 +89,9 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
|
||||
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
|
||||
|
||||
// Give the PC 2 tries to respond before declaring it offline
|
||||
for (int i = 0; i < 2; i++) {
|
||||
// Give the PC 2 tries to respond before declaring it offline if we've seen it before.
|
||||
// If this is an unknown PC, update the status after 1 attempt to get the UI refreshed quickly.
|
||||
for (int i = 0; i < (_host.state == StateUnknown ? 1 : 2); i++) {
|
||||
for (NSString *address in addresses) {
|
||||
if (self.cancelled) {
|
||||
// Get out without updating the status because
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <libxml2/libxml/xmlreader.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
#define SHORT_TIMEOUT_SEC 2
|
||||
#define NORMAL_TIMEOUT_SEC 5
|
||||
#define LONG_TIMEOUT_SEC 60
|
||||
@@ -129,6 +131,10 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
[self executeRequestSynchronously:request];
|
||||
}
|
||||
}
|
||||
else if (_error && request.response) {
|
||||
request.response.statusCode = [_error code];
|
||||
request.response.statusMessage = [_error localizedDescription];
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURLRequest*) createRequestFromString:(NSString*) urlString timeout:(int)timeout {
|
||||
@@ -198,9 +204,10 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
|
||||
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 60 when starting. This won't impact our ability
|
||||
// to get > 60 FPS while actually streaming though.
|
||||
int fps = config.frameRate > 60 ? 60 : config.frameRate;
|
||||
// so force it to 0 to ensure the correct resolution is set. We
|
||||
// used to use 60 here but that locked the frame rate to 60 FPS
|
||||
// on GFE 3.20.3.
|
||||
int fps = config.frameRate > 60 ? 0 : config.frameRate;
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
@@ -210,7 +217,7 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
config.enableHdr ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
|
||||
config.playAudioOnPC ? 1 : 0,
|
||||
(config.audioChannelMask << 16) | config.audioChannelCount,
|
||||
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration),
|
||||
config.gamepadMask, config.gamepadMask];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is launching
|
||||
@@ -221,7 +228,7 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
(config.audioChannelMask << 16) | config.audioChannelCount];
|
||||
SURROUNDAUDIOINFO_FROM_AUDIO_CONFIGURATION(config.audioConfiguration)];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is resuming
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
|
||||
@@ -55,21 +55,26 @@
|
||||
|
||||
xmlChar* statusStr = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_CODE UTF8String]);
|
||||
if (statusStr != NULL) {
|
||||
int status = [[NSString stringWithUTF8String:(const char*)statusStr] intValue];
|
||||
int status = (int)[[NSString stringWithUTF8String:(const char*)statusStr] longLongValue];
|
||||
xmlFree(statusStr);
|
||||
self.statusCode = status;
|
||||
}
|
||||
|
||||
xmlChar* statusMsgXml = xmlGetProp(node, (const xmlChar*)[TAG_STATUS_MESSAGE UTF8String]);
|
||||
NSString* statusMsg;
|
||||
if (statusMsgXml != NULL) {
|
||||
statusMsg = [NSString stringWithUTF8String:(const char*)statusMsgXml];
|
||||
self.statusMessage = [NSString stringWithUTF8String:(const char*)statusMsgXml];
|
||||
xmlFree(statusMsgXml);
|
||||
}
|
||||
else {
|
||||
statusMsg = @"Server Error";
|
||||
self.statusMessage = @"Server Error";
|
||||
}
|
||||
|
||||
if (self.statusCode == -1 && [self.statusMessage isEqualToString:@"Invalid"]) {
|
||||
// Special case handling an audio capture error which GFE doesn't
|
||||
// provide any useful status message for.
|
||||
self.statusCode = 418;
|
||||
self.statusMessage = @"Missing audio capture device. Reinstalling GeForce Experience should resolve this error.";
|
||||
}
|
||||
self.statusMessage = statusMsg;
|
||||
|
||||
node = node->children;
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
- (id) initWithCallback:(id<MDNSCallback>) callback;
|
||||
- (void) searchForHosts;
|
||||
- (void) stopSearching;
|
||||
- (void) forgetHosts;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -63,6 +63,10 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
[mDNSBrowser stop];
|
||||
}
|
||||
|
||||
- (void) forgetHosts {
|
||||
[services removeAllObjects];
|
||||
}
|
||||
|
||||
+ (NSString*)sockAddrToString:(NSData*)addrData {
|
||||
char addrStr[INET6_ADDRSTRLEN];
|
||||
struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
|
||||
@@ -180,19 +184,23 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
continue;
|
||||
}
|
||||
|
||||
// Since we discovered this host over IPv4 mDNS, we know we're on the same network
|
||||
// as the PC and we can use our current WAN address as a likely candidate
|
||||
// for our PC's external address.
|
||||
struct in_addr wanAddr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
|
||||
if (err == 0) {
|
||||
char addrStr[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
|
||||
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
|
||||
Log(LOG_I, @"External IPv4 address (STUN): %@ -> %@", [service hostName], host.externalAddress);
|
||||
}
|
||||
else {
|
||||
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
|
||||
// Don't send a STUN request if we're connected to a VPN. We'll likely get the VPN
|
||||
// gateway's external address rather than the external address of the LAN.
|
||||
if (![Utils isActiveNetworkVPN]) {
|
||||
// Since we discovered this host over IPv4 mDNS, we know we're on the same network
|
||||
// as the PC and we can use our current WAN address as a likely candidate
|
||||
// for our PC's external address.
|
||||
struct in_addr wanAddr;
|
||||
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
|
||||
if (err == 0) {
|
||||
char addrStr[INET_ADDRSTRLEN];
|
||||
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
|
||||
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
|
||||
Log(LOG_I, @"External IPv4 address (STUN): %@ -> %@", [service hostName], host.externalAddress);
|
||||
}
|
||||
else {
|
||||
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
|
||||
}
|
||||
}
|
||||
|
||||
host.localAddress = [MDNSManager sockAddrToString:addrData];
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
@protocol PairCallback <NSObject>
|
||||
|
||||
- (void) showPIN:(NSString*)PIN;
|
||||
- (void) startPairing:(NSString*)PIN;
|
||||
- (void) pairSuccessful:(NSData*)serverCert;
|
||||
- (void) pairFailed:(NSString*)message;
|
||||
- (void) alreadyPaired;
|
||||
@@ -19,8 +19,4 @@
|
||||
|
||||
@interface PairManager : NSOperation
|
||||
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback;
|
||||
- (NSString*) generatePIN;
|
||||
- (NSData*) saltPIN:(NSString*)PIN;
|
||||
- (void) initiatePair:(int)serverMajorVersion;
|
||||
|
||||
@end
|
||||
|
||||
@@ -30,13 +30,13 @@
|
||||
}
|
||||
|
||||
- (void) main {
|
||||
// We have to call startPairing before calling any other _callback functions
|
||||
NSString* PIN = [self generatePIN];
|
||||
[_callback startPairing:PIN];
|
||||
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
|
||||
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
|
||||
if (serverInfoResp == nil) {
|
||||
[_callback pairFailed:@"Unable to connect to PC"];
|
||||
return;
|
||||
}
|
||||
if ([serverInfoResp isStatusOk]) {
|
||||
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
|
||||
[_callback pairFailed:@"You cannot pair while a previous session is still running on the host PC. Quit any running games or reboot the host PC, then try pairing again."];
|
||||
@@ -46,18 +46,30 @@
|
||||
[_callback pairFailed:@"Missing XML element"];
|
||||
return;
|
||||
}
|
||||
[self initiatePair: [[appversion substringToIndex:1] intValue]];
|
||||
[self initiatePairWithPin:PIN forServerMajorVersion:[[appversion substringToIndex:1] intValue]];
|
||||
} else {
|
||||
[_callback alreadyPaired];
|
||||
}
|
||||
}
|
||||
else {
|
||||
[_callback pairFailed:serverInfoResp.statusMessage];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId withError:(NSString*)errorMsg {
|
||||
- (void) finishPairing:(UIBackgroundTaskIdentifier)bgId
|
||||
forResponse:(HttpResponse*)resp
|
||||
withFallbackError:(NSString*)errorMsg {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
|
||||
if (bgId != UIBackgroundTaskInvalid) {
|
||||
[[UIApplication sharedApplication] endBackgroundTask:bgId];
|
||||
}
|
||||
|
||||
if (![resp isStatusOk]) {
|
||||
// Use the response error if the request failed
|
||||
errorMsg = resp.statusMessage;
|
||||
}
|
||||
|
||||
[_callback pairFailed:errorMsg];
|
||||
}
|
||||
|
||||
@@ -70,7 +82,7 @@
|
||||
}
|
||||
|
||||
// All codepaths must call finishPairing exactly once before returning!
|
||||
- (void) initiatePair:(int)serverMajorVersion {
|
||||
- (void) initiatePairWithPin:(NSString*)PIN forServerMajorVersion:(int)serverMajorVersion {
|
||||
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
|
||||
|
||||
// Start a background task to help prevent the app from being killed
|
||||
@@ -79,26 +91,21 @@
|
||||
Log(LOG_W, @"Background pairing time has expired!");
|
||||
}];
|
||||
|
||||
NSString* PIN = [self generatePIN];
|
||||
NSData* salt = [self saltPIN:PIN];
|
||||
Log(LOG_I, @"PIN: %@, saltedPIN: %@", PIN, salt);
|
||||
[_callback showPIN:PIN];
|
||||
NSData* salt = [Utils randomBytes:16];
|
||||
NSData* saltedPIN = [self concatData:salt with:[PIN dataUsingEncoding:NSUTF8StringEncoding]];
|
||||
|
||||
Log(LOG_I, @"PIN: %@, salt %@", PIN, salt);
|
||||
|
||||
HttpResponse* pairResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
|
||||
if (![self verifyResponseStatus:pairResp]) {
|
||||
[self finishPairing:bgId withError:@"Pairing stage #1 failed"];
|
||||
return;
|
||||
}
|
||||
NSInteger pairedStatus;
|
||||
if (![pairResp getIntTag:@"paired" value:&pairedStatus] || !pairedStatus) {
|
||||
[self finishPairing:bgId withError:@"Pairing was declined by the target."];
|
||||
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Pairing was declined by the target."];
|
||||
return;
|
||||
}
|
||||
|
||||
NSString* plainCert = [pairResp getStringTag:@"plaincert"];
|
||||
if ([plainCert length] == 0) {
|
||||
[self finishPairing:bgId withError:@"Another pairing attempt is already in progress."];
|
||||
[self finishPairing:bgId forResponse:pairResp withFallbackError:@"Another pairing attempt is already in progress."];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -112,11 +119,11 @@
|
||||
// Gen 7 servers use SHA256 to get the key
|
||||
int hashLength;
|
||||
if (serverMajorVersion >= 7) {
|
||||
aesKey = [cryptoMan createAESKeyFromSaltSHA256:salt];
|
||||
aesKey = [cryptoMan createAESKeyFromSaltSHA256:saltedPIN];
|
||||
hashLength = 32;
|
||||
}
|
||||
else {
|
||||
aesKey = [cryptoMan createAESKeyFromSaltSHA1:salt];
|
||||
aesKey = [cryptoMan createAESKeyFromSaltSHA1:saltedPIN];
|
||||
hashLength = 20;
|
||||
}
|
||||
|
||||
@@ -125,11 +132,8 @@
|
||||
|
||||
HttpResponse* challengeResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
|
||||
if (![self verifyResponseStatus:challengeResp] ||
|
||||
![challengeResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||
pairedStatus != 1) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Pairing stage #2 failed"];
|
||||
if (![self verifyResponseStatus:challengeResp]) {
|
||||
[self finishPairing:bgId forResponse:challengeResp withFallbackError:@"Pairing stage #2 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -148,15 +152,17 @@
|
||||
else {
|
||||
challengeRespHash = [cryptoMan SHA1HashData: challengeRespHashInput];
|
||||
}
|
||||
NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:challengeRespHash withKey:aesKey];
|
||||
|
||||
assert([challengeRespHash length] <= 32);
|
||||
NSMutableData* paddedHash = [NSMutableData dataWithData:challengeRespHash];
|
||||
[paddedHash setLength:32];
|
||||
|
||||
NSData* challengeRespEncrypted = [cryptoMan aesEncrypt:paddedHash withKey:aesKey];
|
||||
|
||||
HttpResponse* secretResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
|
||||
if (![self verifyResponseStatus:secretResp] ||
|
||||
![secretResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||
pairedStatus != 1) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Pairing stage #3 failed"];
|
||||
if (![self verifyResponseStatus:secretResp]) {
|
||||
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Pairing stage #3 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -165,8 +171,7 @@
|
||||
NSData* serverSignature = [serverSecretResp subdataWithRange:NSMakeRange(16, 256)];
|
||||
|
||||
if (![cryptoMan verifySignature:serverSecret withSignature:serverSignature andCert:[Utils hexToBytes:plainCert]]) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Server certificate invalid"];
|
||||
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Server certificate invalid"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -179,29 +184,22 @@
|
||||
serverChallengeRespHash = [cryptoMan SHA1HashData: serverChallengeRespHashInput];
|
||||
}
|
||||
if (![serverChallengeRespHash isEqual:serverResponse]) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Incorrect PIN"];
|
||||
[self finishPairing:bgId forResponse:secretResp withFallbackError:@"Incorrect PIN"];
|
||||
return;
|
||||
}
|
||||
|
||||
NSData* clientPairingSecret = [self concatData:clientSecret with:[cryptoMan signData:clientSecret withKey:[CryptoManager readKeyFromFile]]];
|
||||
HttpResponse* clientSecretResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientSecretResp withUrlRequest:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]]];
|
||||
if (![self verifyResponseStatus:clientSecretResp] ||
|
||||
![clientSecretResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||
pairedStatus != 1) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Pairing stage #4 failed"];
|
||||
if (![self verifyResponseStatus:clientSecretResp]) {
|
||||
[self finishPairing:bgId forResponse:clientSecretResp withFallbackError:@"Pairing stage #4 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
|
||||
if (![self verifyResponseStatus:clientPairChallengeResp] ||
|
||||
![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||
pairedStatus != 1) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
[self finishPairing:bgId withError:@"Pairing stage #5 failed"];
|
||||
if (![self verifyResponseStatus:clientPairChallengeResp]) {
|
||||
[self finishPairing:bgId forResponse:clientPairChallengeResp withFallbackError:@"Pairing stage #5 failed"];
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -210,14 +208,16 @@
|
||||
|
||||
// Caller calls finishPairing for us on failure
|
||||
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
|
||||
if (resp == nil) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
return false;
|
||||
} else if (![resp isStatusOk]) {
|
||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||
if (![resp isStatusOk]) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
NSInteger pairedStatus;
|
||||
|
||||
if (![resp getIntTag:@"paired" value:&pairedStatus]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return pairedStatus == 1;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,16 +229,9 @@
|
||||
|
||||
- (NSString*) generatePIN {
|
||||
NSString* PIN = [NSString stringWithFormat:@"%d%d%d%d",
|
||||
arc4random() % 10, arc4random() % 10,
|
||||
arc4random() % 10, arc4random() % 10];
|
||||
arc4random_uniform(10), arc4random_uniform(10),
|
||||
arc4random_uniform(10), arc4random_uniform(10)];
|
||||
return PIN;
|
||||
}
|
||||
|
||||
- (NSData*) saltPIN:(NSString*)PIN {
|
||||
NSMutableData* saltedPIN = [[NSMutableData alloc] initWithCapacity:20];
|
||||
[saltedPIN appendData:[Utils randomBytes:16]];
|
||||
[saltedPIN appendBytes:[PIN UTF8String] length:4];
|
||||
return saltedPIN;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -16,8 +16,12 @@
|
||||
|
||||
@implementation WakeOnLanManager
|
||||
|
||||
static const int numPorts = 5;
|
||||
static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
|
||||
static const int numPorts = 7;
|
||||
static const int ports[numPorts] = {
|
||||
9, // Standard WOL port (privileged port)
|
||||
47998, 47999, 48000, 48002, 48010, // Ports opened by GFE
|
||||
47009, // Port opened by Moonlight Internet Hosting Tool for WoL (non-privileged port)
|
||||
};
|
||||
|
||||
+ (void) populateAddress:(struct sockaddr_storage*)addr withPort:(unsigned short)port {
|
||||
if (addr->ss_family == AF_INET) {
|
||||
|
||||
@@ -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 */
|
||||
@@ -9,23 +9,22 @@
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@protocol ConnectionCallbacks <NSObject>
|
||||
#define CONN_TEST_SERVER "ios.conntest.moonlight-stream.org"
|
||||
|
||||
- (void) connectionStarted;
|
||||
- (void) connectionTerminated:(long)errorCode;
|
||||
- (void) stageStarting:(const char*)stageName;
|
||||
- (void) stageComplete:(const char*)stageName;
|
||||
- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
|
||||
- (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;
|
||||
} 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
|
||||
|
||||
+183
-178
@@ -7,11 +7,13 @@
|
||||
//
|
||||
|
||||
#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"
|
||||
|
||||
@@ -24,31 +26,32 @@
|
||||
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;
|
||||
|
||||
#define CIRCULAR_BUFFER_SIZE 16
|
||||
|
||||
static int audioBufferWriteIndex;
|
||||
static int audioBufferReadIndex;
|
||||
static int audioBufferStride;
|
||||
static int audioSamplesPerFrame;
|
||||
static short* audioCircularBuffer;
|
||||
|
||||
#define AUDIO_QUEUE_BUFFERS 4
|
||||
|
||||
static AudioQueueRef audioQueue;
|
||||
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
|
||||
static VideoDecoderRenderer* renderer;
|
||||
|
||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
||||
{
|
||||
[renderer setupWithVideoFormat:videoFormat refreshRate:redrawRate];
|
||||
lastFrameNumber = 0;
|
||||
activeVideoFormat = videoFormat;
|
||||
memset(¤tVideoStats, 0, sizeof(currentVideoStats));
|
||||
memset(&lastVideoStats, 0, sizeof(lastVideoStats));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -57,6 +60,36 @@ void DrCleanup(void)
|
||||
[renderer cleanup];
|
||||
}
|
||||
|
||||
-(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_H265:
|
||||
return @"HEVC";
|
||||
case VIDEO_FORMAT_H265_MAIN10:
|
||||
return @"HEVC Main 10";
|
||||
default:
|
||||
return @"UNKNOWN";
|
||||
}
|
||||
}
|
||||
|
||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
{
|
||||
int offset = 0;
|
||||
@@ -66,12 +99,43 @@ 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(¤tVideoStats, 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;
|
||||
}
|
||||
|
||||
currentVideoStats.receivedFrames++;
|
||||
currentVideoStats.totalFrames++;
|
||||
|
||||
PLENTRY entry = decodeUnit->bufferList;
|
||||
while (entry != NULL) {
|
||||
// Submit parameter set NALUs directly since no copy is required by the decoder
|
||||
if (entry->bufferType != BUFFER_TYPE_PICDATA) {
|
||||
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data length:entry->length bufferType:entry->bufferType];
|
||||
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data
|
||||
length:entry->length
|
||||
bufferType:entry->bufferType
|
||||
frameType:decodeUnit->frameType
|
||||
pts:decodeUnit->presentationTimeMs];
|
||||
if (ret != DR_OK) {
|
||||
free(data);
|
||||
return ret;
|
||||
@@ -86,101 +150,56 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||
}
|
||||
|
||||
// This function will take our picture data buffer
|
||||
return [renderer submitDecodeBuffer:data length:offset bufferType:BUFFER_TYPE_PICDATA];
|
||||
return [renderer submitDecodeBuffer:data
|
||||
length:offset
|
||||
bufferType:BUFFER_TYPE_PICDATA
|
||||
frameType:decodeUnit->frameType
|
||||
pts:decodeUnit->presentationTimeMs];
|
||||
}
|
||||
|
||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
||||
{
|
||||
int err;
|
||||
SDL_AudioSpec want, have;
|
||||
|
||||
// Initialize the circular buffer
|
||||
audioBufferWriteIndex = audioBufferReadIndex = 0;
|
||||
audioSamplesPerFrame = opusConfig->samplesPerFrame;
|
||||
audioBufferStride = opusConfig->channelCount * opusConfig->samplesPerFrame;
|
||||
audioCircularBuffer = malloc(CIRCULAR_BUFFER_SIZE * 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;
|
||||
|
||||
// This is supposed to be a power of 2, but our
|
||||
// frames contain a non-power of 2 number of samples,
|
||||
// so the slop would require buffering another full frame.
|
||||
// Specifying non-Po2 seems to work for our supported platforms.
|
||||
want.samples = opusConfig->samplesPerFrame;
|
||||
|
||||
audioDevice = SDL_OpenAudioDevice(NULL, 0, &want, &have, 0);
|
||||
if (audioDevice == 0) {
|
||||
Log(LOG_E, @"Failed to open audio device: %s\n", SDL_GetError());
|
||||
SDL_QuitSubSystem(SDL_INIT_AUDIO);
|
||||
return -1;
|
||||
}
|
||||
|
||||
audioConfig = *opusConfig;
|
||||
audioFrameSize = opusConfig->samplesPerFrame * sizeof(short) * opusConfig->channelCount;
|
||||
audioBuffer = SDL_malloc(audioFrameSize);
|
||||
|
||||
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||
opusConfig->channelCount,
|
||||
opusConfig->streams,
|
||||
opusConfig->coupledStreams,
|
||||
opusConfig->mapping,
|
||||
&err);
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// Configure the audio session for our app
|
||||
NSError *audioSessionError = nil;
|
||||
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
||||
|
||||
[audioSession setPreferredSampleRate:opusConfig->sampleRate error:&audioSessionError];
|
||||
[audioSession setCategory: AVAudioSessionCategoryPlayback error: &audioSessionError];
|
||||
[audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
|
||||
[audioSession setActive: YES error: &audioSessionError];
|
||||
|
||||
// FIXME: Calling this breaks surround audio for some reason
|
||||
//[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
|
||||
#endif
|
||||
// Start playback
|
||||
SDL_PauseAudioDevice(audioDevice, 0);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// We need to specify a channel layout for surround sound configurations
|
||||
if (opusConfig->channelCount > 2) {
|
||||
AudioChannelLayout channelLayout = {};
|
||||
|
||||
switch (opusConfig->channelCount) {
|
||||
case 6:
|
||||
channelLayout.mChannelLayoutTag = kAudioChannelLayoutTag_MPEG_5_1_A;
|
||||
break;
|
||||
default:
|
||||
// Unsupported channel layout
|
||||
Log(LOG_E, @"Unsupported channel layout: %d\n", opusConfig->channelCount);
|
||||
abort();
|
||||
}
|
||||
|
||||
status = AudioQueueSetProperty(audioQueue, kAudioQueueProperty_ChannelLayout, &channelLayout, sizeof(channelLayout));
|
||||
if (status != noErr) {
|
||||
Log(LOG_E, @"Error configuring surround channel layout: %d\n", status);
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
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]);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -190,45 +209,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;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// Audio session is now inactive
|
||||
[[AVAudioSession sharedInstance] setActive: NO error: nil];
|
||||
#endif
|
||||
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) % CIRCULAR_BUFFER_SIZE) == 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) % CIRCULAR_BUFFER_SIZE;
|
||||
if (SDL_QueueAudio(audioDevice,
|
||||
audioBuffer,
|
||||
sizeof(short) * decodeLen * audioConfig.channelCount) < 0) {
|
||||
Log(LOG_E, @"Failed to queue audio sample: %s\n", SDL_GetError());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -242,9 +259,9 @@ void ClStageComplete(int stage)
|
||||
[_callbacks stageComplete:LiGetStageName(stage)];
|
||||
}
|
||||
|
||||
void ClStageFailed(int stage, long errorCode)
|
||||
void ClStageFailed(int stage, int errorCode)
|
||||
{
|
||||
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode];
|
||||
[_callbacks stageFailed:LiGetStageName(stage) withError:errorCode portTestFlags:LiGetPortFlagsFromStage(stage)];
|
||||
}
|
||||
|
||||
void ClConnectionStarted(void)
|
||||
@@ -252,7 +269,7 @@ void ClConnectionStarted(void)
|
||||
[_callbacks connectionStarted];
|
||||
}
|
||||
|
||||
void ClConnectionTerminated(long errorCode)
|
||||
void ClConnectionTerminated(int errorCode)
|
||||
{
|
||||
[_callbacks connectionTerminated: errorCode];
|
||||
}
|
||||
@@ -303,6 +320,10 @@ void ClConnectionStatusUpdate(int status)
|
||||
initLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
if (videoStatsLock == nil) {
|
||||
videoStatsLock = [[NSLock alloc] init];
|
||||
}
|
||||
|
||||
strncpy(_hostString,
|
||||
[config.host cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_hostString));
|
||||
@@ -314,6 +335,11 @@ void ClConnectionStatusUpdate(int status)
|
||||
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_gfeVersionString));
|
||||
}
|
||||
if (config.rtspSessionUrl != nil) {
|
||||
strncpy(_rtspSessionUrl,
|
||||
[config.rtspSessionUrl cStringUsingEncoding:NSUTF8StringEncoding],
|
||||
sizeof(_rtspSessionUrl));
|
||||
}
|
||||
|
||||
LiInitializeServerInformation(&_serverInfo);
|
||||
_serverInfo.address = _hostString;
|
||||
@@ -321,6 +347,9 @@ void ClConnectionStatusUpdate(int status)
|
||||
if (config.gfeVersion != nil) {
|
||||
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
|
||||
}
|
||||
if (config.rtspSessionUrl != nil) {
|
||||
_serverInfo.rtspSessionUrl = _rtspSessionUrl;
|
||||
}
|
||||
|
||||
renderer = myRenderer;
|
||||
_callbacks = callbacks;
|
||||
@@ -331,49 +360,50 @@ void ClConnectionStatusUpdate(int status)
|
||||
_streamConfig.fps = config.frameRate;
|
||||
_streamConfig.bitrate = config.bitRate;
|
||||
_streamConfig.enableHdr = config.enableHdr;
|
||||
_streamConfig.audioConfiguration = config.audioConfiguration;
|
||||
|
||||
// TODO: If/when video encryption is added, we'll probably want to
|
||||
// limit that to devices that support the ARMv8 AES instructions.
|
||||
_streamConfig.encryptionFlags = ENCFLG_AUDIO;
|
||||
|
||||
// Use some of the HEVC encoding efficiency improvements to
|
||||
// reduce bandwidth usage while still gaining some image
|
||||
// quality improvement.
|
||||
_streamConfig.hevcBitratePercentageMultiplier = 75;
|
||||
|
||||
// Detect remote streaming automatically based on the IP address of the target
|
||||
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
_streamConfig.packetSize = 1392;
|
||||
|
||||
switch (config.audioChannelCount) {
|
||||
case 2:
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
break;
|
||||
case 6:
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
break;
|
||||
default:
|
||||
Log(LOG_E, @"Unknown audio channel count: %d", config.audioChannelCount);
|
||||
abort();
|
||||
if ([Utils isActiveNetworkVPN]) {
|
||||
// Force remote streaming mode when a VPN is connected
|
||||
_streamConfig.streamingRemotely = STREAM_CFG_REMOTE;
|
||||
_streamConfig.packetSize = 1024;
|
||||
}
|
||||
else {
|
||||
// Detect remote streaming automatically based on the IP address of the target
|
||||
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||
_streamConfig.packetSize = 1392;
|
||||
}
|
||||
|
||||
// HDR implies HEVC allowed
|
||||
if (config.enableHdr) {
|
||||
config.allowHevc = YES;
|
||||
}
|
||||
|
||||
// Streaming at resolutions above 4K requires HEVC
|
||||
if (config.width > 4096 || config.height > 4096) {
|
||||
config.allowHevc = YES;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// On iOS 11, we can use HEVC if the server supports encoding it
|
||||
// and this device has hardware decode for it (A9 and later).
|
||||
// Additionally, iPhone X had a bug which would cause video
|
||||
// to freeze after a few minutes with HEVC prior to iOS 11.3.
|
||||
// As a result, we will only use HEVC on iOS 11.3 or later.
|
||||
if (@available(iOS 11.3, tvOS 11.3, *)) {
|
||||
_streamConfig.supportsHevc = config.allowHevc && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
|
||||
}
|
||||
#else
|
||||
if (@available(macOS 10.13, *)) {
|
||||
// Streaming with limited bandwidth will result in better quality with HEVC
|
||||
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || _streamConfig.streamingRemotely != 0)
|
||||
_streamConfig.supportsHevc = config.allowHevc;
|
||||
}
|
||||
_streamConfig.supportsHevc =
|
||||
#if !TARGET_OS_TV
|
||||
config.allowHevc &&
|
||||
#endif
|
||||
VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
|
||||
}
|
||||
|
||||
// HEVC must be supported when HDR is enabled
|
||||
assert(!_streamConfig.enableHdr || _streamConfig.supportsHevc);
|
||||
@@ -386,18 +416,22 @@ 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;
|
||||
// RFI seems to be broken at all resolutions on the Apple TV 4K (1st gen)
|
||||
// on tvOS 14.5.
|
||||
_drCallbacks.capabilities =
|
||||
#if !TARGET_OS_TV
|
||||
CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
|
||||
#endif
|
||||
CAPABILITY_PULL_RENDERER;
|
||||
|
||||
LiInitializeAudioCallbacks(&_arCallbacks);
|
||||
_arCallbacks.init = ArInit;
|
||||
_arCallbacks.cleanup = ArCleanup;
|
||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
||||
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
|
||||
_arCallbacks.capabilities = CAPABILITY_SUPPORTS_ARBITRARY_AUDIO_DURATION;
|
||||
|
||||
LiInitializeConnectionCallbacks(&_clCallbacks);
|
||||
_clCallbacks.stageStarting = ClStageStarting;
|
||||
@@ -412,35 +446,6 @@ void ClConnectionStatusUpdate(int status)
|
||||
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) % CIRCULAR_BUFFER_SIZE;
|
||||
}
|
||||
else {
|
||||
// No data, so play silence
|
||||
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
|
||||
}
|
||||
|
||||
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||
}
|
||||
|
||||
-(void) main
|
||||
{
|
||||
[initLock lock];
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// 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) videoContentShown;
|
||||
|
||||
@end
|
||||
@@ -13,18 +13,17 @@
|
||||
@property NSString* gfeVersion;
|
||||
@property NSString* appID;
|
||||
@property NSString* appName;
|
||||
@property NSString* rtspSessionUrl;
|
||||
@property int width;
|
||||
@property int height;
|
||||
@property int frameRate;
|
||||
@property int bitRate;
|
||||
@property int riKeyId;
|
||||
@property BOOL streamingRemotely;
|
||||
@property NSData* riKey;
|
||||
@property int gamepadMask;
|
||||
@property BOOL optimizeGameSettings;
|
||||
@property BOOL playAudioOnPC;
|
||||
@property int audioChannelCount;
|
||||
@property int audioChannelMask;
|
||||
@property int audioConfiguration;
|
||||
@property BOOL enableHdr;
|
||||
@property BOOL multiController;
|
||||
@property BOOL allowHevc;
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@implementation StreamConfiguration
|
||||
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioChannelMask, audioChannelCount, enableHdr, multiController, allowHevc, serverCert;
|
||||
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, audioConfiguration, enableHdr, multiController, allowHevc, serverCert, rtspSessionUrl;
|
||||
@end
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
@@ -50,7 +52,11 @@
|
||||
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
||||
NSString* gfeVersion = [serverInfoResp getStringTag:@"GfeVersion"];
|
||||
NSString* serverState = [serverInfoResp getStringTag:@"state"];
|
||||
if (![serverInfoResp isStatusOk] || pairStatus == NULL || appversion == NULL || serverState == NULL) {
|
||||
if (![serverInfoResp isStatusOk]) {
|
||||
[_callbacks launchFailed:serverInfoResp.statusMessage];
|
||||
return;
|
||||
}
|
||||
else if (pairStatus == NULL || appversion == NULL || serverState == NULL) {
|
||||
[_callbacks launchFailed:@"Failed to connect to PC"];
|
||||
return;
|
||||
}
|
||||
@@ -61,27 +67,32 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (_config.width > 4096 || _config.height > 4096) {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// resumeApp and launchApp handle calling launchFailed
|
||||
NSString* sessionUrl;
|
||||
if ([serverState hasSuffix:@"_SERVER_BUSY"]) {
|
||||
// App already running, resume it
|
||||
if (![self resumeApp:hMan]) {
|
||||
if (![self resumeApp:hMan receiveSessionUrl:&sessionUrl]) {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
// Start app
|
||||
if (![self launchApp:hMan]) {
|
||||
if (![self launchApp:hMan receiveSessionUrl:&sessionUrl]) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
// Set mouse delta factors from the screen resolution and stream size
|
||||
CGFloat screenScale = [[UIScreen mainScreen] scale];
|
||||
CGRect screenBounds = [[UIScreen mainScreen] bounds];
|
||||
CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);
|
||||
[((StreamView*)_renderView) setMouseDeltaFactors:_config.width / screenSize.width
|
||||
y:_config.height / screenSize.height];
|
||||
#endif
|
||||
|
||||
// Populate RTSP session URL from launch/resume response
|
||||
_config.rtspSessionUrl = sessionUrl;
|
||||
|
||||
// Populate the config's version fields from serverinfo
|
||||
_config.appVersion = appversion;
|
||||
@@ -89,7 +100,7 @@
|
||||
|
||||
// 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];
|
||||
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:self->_connection];
|
||||
@@ -101,7 +112,7 @@
|
||||
[_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]]];
|
||||
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
|
||||
@@ -115,10 +126,11 @@
|
||||
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]]];
|
||||
NSString* resume = [resumeResp getStringTag:@"resume"];
|
||||
@@ -132,7 +144,38 @@
|
||||
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";
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
|
||||
@import AVFoundation;
|
||||
|
||||
#import "ConnectionCallbacks.h"
|
||||
|
||||
@interface VideoDecoderRenderer : NSObject
|
||||
|
||||
- (id)initWithView:(OSView*)view;
|
||||
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks;
|
||||
|
||||
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate;
|
||||
|
||||
@@ -18,6 +20,6 @@
|
||||
|
||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
|
||||
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType;
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType frameType:(int)frameType pts:(unsigned int)pts;
|
||||
|
||||
@end
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
@implementation VideoDecoderRenderer {
|
||||
StreamView* _view;
|
||||
id<ConnectionCallbacks> _callbacks;
|
||||
|
||||
AVSampleBufferDisplayLayer* displayLayer;
|
||||
Boolean waitingForSps, waitingForPps, waitingForVps;
|
||||
@@ -30,7 +31,7 @@
|
||||
|
||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||
displayLayer.bounds = _view.bounds;
|
||||
displayLayer.backgroundColor = [OSColor blackColor].CGColor;
|
||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
||||
|
||||
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
||||
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||
@@ -61,11 +62,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (id)initWithView:(StreamView*)view
|
||||
- (id)initWithView:(StreamView*)view callbacks:(id<ConnectionCallbacks>)callbacks
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_view = view;
|
||||
_callbacks = callbacks;
|
||||
|
||||
[self reinitializeDisplayLayer];
|
||||
|
||||
@@ -75,25 +77,32 @@
|
||||
- (void)setupWithVideoFormat:(int)videoFormat refreshRate:(int)refreshRate
|
||||
{
|
||||
self->videoFormat = videoFormat;
|
||||
#if !TARGET_OS_IPHONE
|
||||
_view.codec = videoFormat;
|
||||
#endif
|
||||
|
||||
if (refreshRate > 60) {
|
||||
// HACK: We seem to just get 60 Hz screen updates even with a 120 FPS stream if
|
||||
// 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];
|
||||
_displayLink = [CADisplayLink displayLinkWithTarget:self selector:@selector(displayLinkCallback:)];
|
||||
if (@available(iOS 10.0, tvOS 10.0, *)) {
|
||||
_displayLink.preferredFramesPerSecond = refreshRate;
|
||||
}
|
||||
[_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];
|
||||
}
|
||||
|
||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
|
||||
|
||||
- (void)displayLinkCallback:(CADisplayLink *)sender
|
||||
{
|
||||
// No-op - rendering done in submitDecodeBuffer
|
||||
VIDEO_FRAME_HANDLE handle;
|
||||
PDECODE_UNIT du;
|
||||
|
||||
while (LiPollNextVideoFrame(&handle, &du)) {
|
||||
LiCompleteVideoFrame(handle, DrSubmitDecodeUnit(du));
|
||||
|
||||
#if 0
|
||||
// Always keep one pending frame smooth out gaps due to
|
||||
// network jitter at the cost of 1 frame of latency
|
||||
if (LiGetPendingVideoFrames() == 1) {
|
||||
break;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cleanup
|
||||
@@ -116,27 +125,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
- (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
|
||||
{
|
||||
OSStatus status;
|
||||
@@ -204,9 +192,8 @@
|
||||
}
|
||||
|
||||
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType
|
||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType frameType:(int)frameType pts:(unsigned int)pts
|
||||
{
|
||||
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
|
||||
OSStatus status;
|
||||
|
||||
if (bufferType != BUFFER_TYPE_PICDATA) {
|
||||
@@ -255,11 +242,7 @@
|
||||
|
||||
Log(LOG_I, @"Constructing new HEVC format description");
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if (@available(iOS 11.0, *)) {
|
||||
#else
|
||||
if (@available(macOS 10.13, *)) {
|
||||
#endif
|
||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||
3, /* count of parameter sets */
|
||||
parameterSetPointers,
|
||||
@@ -360,7 +343,7 @@
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DisplayImmediately, kCFBooleanTrue);
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_IsDependedOnByOthers, kCFBooleanTrue);
|
||||
|
||||
if (![self isNalReferencePicture:nalType]) {
|
||||
if (frameType == FRAME_TYPE_PFRAME) {
|
||||
// P-frame
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_NotSync, kCFBooleanTrue);
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanTrue);
|
||||
@@ -370,24 +353,20 @@
|
||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
|
||||
}
|
||||
|
||||
#if !TARGET_OS_IPHONE
|
||||
_view.frameCount++;
|
||||
#endif
|
||||
|
||||
// Enqueue video samples on the main thread
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Enqueue the next frame
|
||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
// Enqueue the next frame
|
||||
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||
|
||||
if (frameType == FRAME_TYPE_IDR) {
|
||||
// Ensure the layer is visible now
|
||||
self->displayLayer.hidden = NO;
|
||||
|
||||
if ([self isNalReferencePicture:nalType]) {
|
||||
// Ensure the layer is visible now
|
||||
self->displayLayer.hidden = NO;
|
||||
}
|
||||
|
||||
// Dereference the buffers
|
||||
CFRelease(blockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
});
|
||||
// Tell our parent VC to hide the progress indicator
|
||||
[self->_callbacks videoContentShown];
|
||||
}
|
||||
|
||||
// Dereference the buffers
|
||||
CFRelease(blockBuffer);
|
||||
CFRelease(sampleBuffer);
|
||||
|
||||
return DR_OK;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@interface UIAppView : UIView
|
||||
#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;
|
||||
|
||||
+132
-88
@@ -9,13 +9,13 @@
|
||||
#import "UIAppView.h"
|
||||
#import "AppAssetManager.h"
|
||||
|
||||
static const float REFRESH_CYCLE = 2.0f;
|
||||
static const float REFRESH_CYCLE = 1.0f;
|
||||
|
||||
@implementation UIAppView {
|
||||
TemporaryApp* _app;
|
||||
UIButton* _appButton;
|
||||
UILabel* _appLabel;
|
||||
UIImageView* _appOverlay;
|
||||
UIImageView* _appImage;
|
||||
NSCache* _artCache;
|
||||
id<AppCallback> _callback;
|
||||
}
|
||||
@@ -33,55 +33,84 @@ static UIImage* noImage;
|
||||
if (noImage == nil) {
|
||||
noImage = [UIImage imageNamed:@"NoAppImage"];
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
self.frame = CGRectMake(0, 0, 200, 265);
|
||||
#else
|
||||
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];
|
||||
|
||||
// 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(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];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
_appButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||
_appImage.adjustsImageWhenAncestorFocused = YES;
|
||||
#else
|
||||
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
#endif
|
||||
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
|
||||
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||
[_appButton sizeToFit];
|
||||
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else {
|
||||
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
|
||||
[self addSubview:_appButton];
|
||||
[self sizeToFit];
|
||||
|
||||
// Rasterizing the cell layer increases rendering performance by quite a bit
|
||||
// but we want it unrasterized for tvOS where it must be scaled.
|
||||
self.layer.shouldRasterize = YES;
|
||||
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
||||
|
||||
if (@available(iOS 13.4.1, *)) {
|
||||
// Allow the button style to change when moused over
|
||||
self.pointerInteractionEnabled = YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
[self updateAppImage];
|
||||
[self startUpdateLoop];
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (UIImageView*) renderToImageView:(UIImage*)appImage {
|
||||
//custom image to do TvOS hover popup effect
|
||||
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
|
||||
imageView.userInteractionEnabled = YES;
|
||||
imageView.adjustsImageWhenAncestorFocused = YES;
|
||||
imageView.frame = self.frame;
|
||||
UIGraphicsBeginImageContextWithOptions(self.frame.size, false, 0);
|
||||
[imageView.layer renderInContext:(UIGraphicsGetCurrentContext())];
|
||||
[_appLabel.layer renderInContext:(UIGraphicsGetCurrentContext())];
|
||||
[_appOverlay.layer renderInContext:(UIGraphicsGetCurrentContext())];
|
||||
UIImage *imageWithText = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
[imageView setImage:imageWithText];
|
||||
[_appButton addSubview:imageView];
|
||||
return imageView;
|
||||
- (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
|
||||
|
||||
@@ -90,28 +119,9 @@ static UIImage* noImage;
|
||||
[_appOverlay removeFromSuperview];
|
||||
_appOverlay = nil;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
_appButton.frame = CGRectMake(0, 0, 200, 265);
|
||||
#else
|
||||
_appButton.frame = CGRectMake(0, 0, 150, 200);
|
||||
#endif
|
||||
|
||||
self.frame = _appButton.frame;
|
||||
|
||||
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
||||
// Only create the app overlay if needed
|
||||
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
||||
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
||||
_appOverlay.layer.shadowOpacity = 1;
|
||||
_appOverlay.layer.shadowRadius = 4.0;
|
||||
#if TARGET_OS_TV
|
||||
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
|
||||
_appOverlay.frame = CGRectMake(0, 0, self.frame.size.width, self.frame.size.height / 3.f);
|
||||
#else
|
||||
_appOverlay.frame = CGRectMake(self.frame.size.width / 4, 10, self.frame.size.width / 2, self.frame.size.height / 4.f);
|
||||
#endif
|
||||
if (_appLabel != nil) {
|
||||
[_appLabel removeFromSuperview];
|
||||
_appLabel = nil;
|
||||
}
|
||||
|
||||
BOOL noAppImage = false;
|
||||
@@ -125,20 +135,13 @@ static UIImage* noImage;
|
||||
[_artCache setObject:appImage forKey:_app];
|
||||
}
|
||||
}
|
||||
|
||||
if (appImage != nil) {
|
||||
// This size of image might be blank image received from GameStream.
|
||||
// TODO: Improve no-app image detection
|
||||
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
|
||||
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[_appButton addSubview:[self renderToImageView:appImage]];
|
||||
#else
|
||||
[self addSubview:_appOverlay];
|
||||
#endif
|
||||
|
||||
[_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
|
||||
[self setNeedsDisplay];
|
||||
[_appImage setImage:appImage];
|
||||
} else {
|
||||
noAppImage = true;
|
||||
}
|
||||
@@ -146,48 +149,89 @@ static UIImage* noImage;
|
||||
noAppImage = true;
|
||||
}
|
||||
|
||||
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
||||
// Only create the app overlay if needed
|
||||
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
||||
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
||||
_appOverlay.layer.shadowOpacity = 1;
|
||||
_appOverlay.layer.shadowRadius = 4.0;
|
||||
_appOverlay.contentMode = UIViewContentModeScaleAspectFit;
|
||||
}
|
||||
|
||||
if (noAppImage) {
|
||||
#if TARGET_OS_TV
|
||||
_appLabel = [[UILabel alloc] init];
|
||||
[_appLabel setTextColor:[UIColor whiteColor]];
|
||||
[_appLabel setText:_app.name];
|
||||
[_appLabel setFrame:self.frame];
|
||||
#else
|
||||
_appLabel = _appButton.titleLabel;
|
||||
[_appButton setTitleColor:[UIColor whiteColor] forState:UIControlStateNormal];
|
||||
[_appButton setTitle:_app.name forState:UIControlStateNormal];
|
||||
[_appButton setTitleEdgeInsets:UIEdgeInsetsMake(_appOverlay != nil ? _appOverlay.frame.size.height : 5, 5, 5, 5)];
|
||||
#endif
|
||||
[_appLabel setFont:[UIFont systemFontOfSize:24]];
|
||||
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
|
||||
[_appLabel setTextAlignment:NSTextAlignmentCenter];
|
||||
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
|
||||
[_appLabel setNumberOfLines:0];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[_appButton addSubview:[self renderToImageView:noImage]];
|
||||
#else
|
||||
[self addSubview:_appOverlay];
|
||||
#endif
|
||||
}
|
||||
|
||||
[self positionSubviews];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[_appImage.overlayContentView addSubview:_appLabel];
|
||||
[_appImage.overlayContentView addSubview:_appOverlay];
|
||||
#else
|
||||
[self addSubview:_appLabel];
|
||||
[self addSubview:_appOverlay];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) startUpdateLoop {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
- (void) buttonSelected:(id)sender {
|
||||
_appImage.layer.opacity = 0.5f;
|
||||
}
|
||||
- (void) buttonDeselected:(id)sender {
|
||||
_appImage.layer.opacity = 1.0f;
|
||||
}
|
||||
|
||||
- (void) positionSubviews {
|
||||
CGFloat padding = 5.f;
|
||||
CGSize frameSize = _appImage.frame.size;
|
||||
CGPoint center = _appImage.center;
|
||||
|
||||
if (_appLabel != nil) {
|
||||
if (_appOverlay != nil) {
|
||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 3, frameSize.width / 3);
|
||||
_appOverlay.center = CGPointMake(frameSize.width / 2, padding + _appOverlay.frame.size.height / 2);
|
||||
|
||||
[_appLabel setFrame:CGRectMake(padding, _appOverlay.frame.size.height + padding, frameSize.width - 2 * padding, frameSize.height - _appOverlay.frame.size.height - 2 * padding)];
|
||||
}
|
||||
else {
|
||||
[_appLabel setFrame:CGRectMake(padding, padding, frameSize.width - 2 * padding, frameSize.height - 2 * padding)];
|
||||
}
|
||||
}
|
||||
else if (_appOverlay != nil) {
|
||||
_appOverlay.frame = CGRectMake(0, 0, frameSize.width / 2, frameSize.width / 2);
|
||||
_appOverlay.center = center;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) 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
|
||||
|
||||
@@ -17,7 +17,11 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface UIComputerView : UIView
|
||||
#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;
|
||||
|
||||
+88
-64
@@ -10,7 +10,7 @@
|
||||
|
||||
@implementation UIComputerView {
|
||||
TemporaryHost* _host;
|
||||
UIButton* _hostButton;
|
||||
UIImageView* _hostIcon;
|
||||
UILabel* _hostLabel;
|
||||
UIImageView* _hostOverlay;
|
||||
UIActivityIndicatorView* _hostSpinner;
|
||||
@@ -29,50 +29,71 @@ static const int LABEL_DY = 20;
|
||||
|
||||
- (id) init {
|
||||
self = [super init];
|
||||
_hostButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
|
||||
[_hostButton sizeToFit];
|
||||
|
||||
|
||||
#if TARGET_OS_TV
|
||||
_hostButton.frame = CGRectMake(0, 0, 400, 400);
|
||||
self.frame = CGRectMake(0, 0, 400, 400);
|
||||
#else
|
||||
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
|
||||
_hostButton.frame = CGRectMake(0, 0, 200, 200);
|
||||
self.frame = CGRectMake(0, 0, 200, 200);
|
||||
} else {
|
||||
_hostButton.frame = CGRectMake(0, 0, 100, 100);
|
||||
self.frame = CGRectMake(0, 0, 100, 100);
|
||||
}
|
||||
#endif
|
||||
|
||||
_hostButton.layer.shadowColor = [[UIColor blackColor] CGColor];
|
||||
_hostButton.layer.shadowOffset = CGSizeMake(5,8);
|
||||
_hostButton.layer.shadowOpacity = 0.3;
|
||||
_hostIcon = [[UIImageView alloc] initWithFrame:self.frame];
|
||||
[_hostIcon setImage:[UIImage imageNamed:@"Computer"]];
|
||||
|
||||
self.layer.shadowColor = [[UIColor blackColor] CGColor];
|
||||
self.layer.shadowOffset = CGSizeMake(5,8);
|
||||
self.layer.shadowOpacity = 0.3;
|
||||
|
||||
[_hostButton addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
|
||||
[_hostButton addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
||||
[self addTarget:self action:@selector(hostButtonSelected:) forControlEvents:UIControlEventTouchDown];
|
||||
[self addTarget:self action:@selector(hostButtonDeselected:) forControlEvents:UIControlEventTouchUpInside | UIControlEventTouchCancel | UIControlEventTouchDragExit];
|
||||
|
||||
_hostLabel = [[UILabel alloc] init];
|
||||
_hostLabel.textColor = [UIColor whiteColor];
|
||||
|
||||
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(_hostButton.frame.size.width / 3, _hostButton.frame.size.height / 4, _hostButton.frame.size.width / 3, _hostButton.frame.size.height / 3)];
|
||||
_hostOverlay = [[UIImageView alloc] initWithFrame:CGRectMake(self.frame.size.width / 3, _hostIcon.frame.size.height / 4, _hostIcon.frame.size.width / 3, self.frame.size.height / 3)];
|
||||
_hostSpinner = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
[_hostSpinner setFrame:_hostOverlay.frame];
|
||||
_hostSpinner.userInteractionEnabled = NO;
|
||||
_hostSpinner.hidesWhenStopped = YES;
|
||||
|
||||
[self addSubview:_hostButton];
|
||||
[self addSubview:_hostLabel];
|
||||
[self addSubview:_hostIcon];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
_hostIcon.clipsToBounds = NO;
|
||||
_hostIcon.adjustsImageWhenAncestorFocused = YES;
|
||||
_hostIcon.masksFocusEffectToContents = YES;
|
||||
|
||||
self.adjustsImageWhenHighlighted = NO;
|
||||
|
||||
_hostOverlay.masksFocusEffectToContents = YES;
|
||||
_hostOverlay.adjustsImageWhenAncestorFocused = NO;
|
||||
|
||||
[_hostIcon.overlayContentView addSubview:_hostOverlay];
|
||||
[_hostIcon.overlayContentView addSubview:_hostSpinner];
|
||||
#else
|
||||
[self addSubview:_hostOverlay];
|
||||
[self addSubview:_hostSpinner];
|
||||
|
||||
if (@available(iOS 13.4.1, *)) {
|
||||
// Allow the button style to change when moused over
|
||||
self.pointerInteractionEnabled = YES;
|
||||
}
|
||||
#endif
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) hostButtonSelected:(id)sender {
|
||||
_hostIcon.layer.opacity = 0.5f;
|
||||
_hostSpinner.layer.opacity = 0.5f;
|
||||
_hostOverlay.layer.opacity = 0.5f;
|
||||
}
|
||||
- (void) hostButtonDeselected:(id)sender {
|
||||
_hostIcon.layer.opacity = 1.0f;
|
||||
_hostSpinner.layer.opacity = 1.0f;
|
||||
_hostOverlay.layer.opacity = 1.0f;
|
||||
}
|
||||
@@ -81,19 +102,11 @@ static const int LABEL_DY = 20;
|
||||
self = [self init];
|
||||
_callback = callback;
|
||||
|
||||
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else {
|
||||
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
[self addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[_hostLabel setText:@"Add Host"];
|
||||
[_hostLabel setText:@"Add Host Manually"];
|
||||
[_hostLabel sizeToFit];
|
||||
|
||||
float x = _hostButton.frame.origin.x + _hostButton.frame.size.width / 2;
|
||||
_hostLabel.center = CGPointMake(x, _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
|
||||
|
||||
[_hostOverlay setImage:[UIImage imageNamed:@"AddOverlayIcon"]];
|
||||
|
||||
[self updateBounds];
|
||||
@@ -106,39 +119,53 @@ static const int LABEL_DY = 20;
|
||||
_host = host;
|
||||
_callback = callback;
|
||||
|
||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
||||
[_hostButton 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, *)) {
|
||||
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
}
|
||||
else {
|
||||
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||
}
|
||||
[self addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||
|
||||
[self updateContentsForHost:host];
|
||||
[self updateBounds];
|
||||
[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;
|
||||
float width = 0;
|
||||
float height;
|
||||
|
||||
x = MIN(x, _hostButton.frame.origin.x);
|
||||
float iconX = _hostIcon.frame.origin.x + _hostIcon.frame.size.width / 2;
|
||||
_hostLabel.center = CGPointMake(iconX, _hostIcon.frame.origin.y + _hostIcon.frame.size.height + LABEL_DY);
|
||||
|
||||
x = MIN(x, _hostIcon.frame.origin.x);
|
||||
x = MIN(x, _hostLabel.frame.origin.x);
|
||||
|
||||
y = MIN(y, _hostButton.frame.origin.y);
|
||||
y = MIN(y, _hostIcon.frame.origin.y);
|
||||
y = MIN(y, _hostLabel.frame.origin.y);
|
||||
|
||||
width = MAX(width, _hostButton.frame.size.width);
|
||||
width = MAX(width, _hostIcon.frame.size.width);
|
||||
width = MAX(width, _hostLabel.frame.size.width);
|
||||
|
||||
height = _hostButton.frame.size.height +
|
||||
height = _hostIcon.frame.size.height +
|
||||
_hostLabel.frame.size.height +
|
||||
LABEL_DY / 2;
|
||||
|
||||
@@ -167,21 +194,19 @@ static const int LABEL_DY = 20;
|
||||
[_hostSpinner startAnimating];
|
||||
}
|
||||
|
||||
float x = _hostButton.frame.origin.x + _hostButton.frame.size.width / 2;
|
||||
_hostLabel.center = CGPointMake(x, _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
|
||||
}
|
||||
|
||||
- (void) startUpdateLoop {
|
||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||
[self updateBounds];
|
||||
}
|
||||
|
||||
- (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 {
|
||||
@@ -190,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];
|
||||
}
|
||||
@@ -198,18 +236,4 @@ static const int LABEL_DY = 20;
|
||||
[_callback addHostClicked];
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
- (void) didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
|
||||
UIButton *previousButton = (UIButton *)context.previouslyFocusedItem;
|
||||
UIButton *nextButton = (UIButton *) context.nextFocusedItem;
|
||||
|
||||
if (previousButton.superview == self) {
|
||||
self.backgroundColor = nil;
|
||||
}
|
||||
if (nextButton.superview == self) {
|
||||
nextButton.superview.backgroundColor = [UIColor darkGrayColor];
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -25,8 +25,8 @@ FOUNDATION_EXPORT NSString *const deviceName;
|
||||
+ (NSData*) randomBytes:(NSInteger)length;
|
||||
+ (NSString*) bytesToHex:(NSData*)data;
|
||||
+ (NSData*) hexToBytes:(NSString*) hex;
|
||||
+ (int) resolveHost:(NSString*)host;
|
||||
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
|
||||
+ (BOOL) isActiveNetworkVPN;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
+10
-19
@@ -50,27 +50,18 @@ NSString *const deviceName = @"roth";
|
||||
return hex;
|
||||
}
|
||||
|
||||
+ (int) resolveHost:(NSString*)host {
|
||||
struct hostent *hostent;
|
||||
|
||||
if (inet_addr([host UTF8String]) != INADDR_NONE) {
|
||||
// Already an IP address
|
||||
int addr = inet_addr([host UTF8String]);
|
||||
Log(LOG_I, @"host address: %d", addr);
|
||||
return addr;
|
||||
} else {
|
||||
hostent = gethostbyname([host UTF8String]);
|
||||
if (hostent != NULL) {
|
||||
char* ipstr = inet_ntoa(*(struct in_addr*)hostent->h_addr_list[0]);
|
||||
Log(LOG_I, @"Resolved %@ -> %s", host, ipstr);
|
||||
int addr = inet_addr(ipstr);
|
||||
Log(LOG_I, @"host address: %d", addr);
|
||||
return addr;
|
||||
} else {
|
||||
Log(LOG_W, @"Failed to resolve host: %d", h_errno);
|
||||
return 0;
|
||||
+ (BOOL)isActiveNetworkVPN {
|
||||
NSDictionary *dict = CFBridgingRelease(CFNetworkCopySystemProxySettings());
|
||||
NSArray *keys = [dict[@"__SCOPED__"] allKeys];
|
||||
for (NSString *key in keys) {
|
||||
if ([key containsString:@"tap"] ||
|
||||
[key containsString:@"tun"] ||
|
||||
[key containsString:@"ppp"] ||
|
||||
[key containsString:@"ipsec"]) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
|
||||
|
||||
@@ -17,8 +17,8 @@
|
||||
|
||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *settingsButton;
|
||||
#if !TARGET_OS_TV
|
||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *upButton;
|
||||
#endif
|
||||
|
||||
|
||||
@@ -29,13 +29,18 @@
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
#import "SettingsViewController.h"
|
||||
#else
|
||||
#import <sys/utsname.h>
|
||||
#endif
|
||||
|
||||
#import <VideoToolbox/VideoToolbox.h>
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation MainFrameViewController {
|
||||
NSOperationQueue* _opQueue;
|
||||
TemporaryHost* _selectedHost;
|
||||
BOOL _showHiddenApps;
|
||||
NSString* _uniqueId;
|
||||
NSData* _clientCert;
|
||||
DiscoveryManager* _discMan;
|
||||
@@ -54,45 +59,47 @@
|
||||
}
|
||||
static NSMutableSet* hostList;
|
||||
|
||||
- (void)showPIN:(NSString *)PIN {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
- (void)startPairing:(NSString *)PIN {
|
||||
// Needs to be synchronous to ensure the alert is shown before any potential
|
||||
// failure callback could be invoked.
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
|
||||
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
|
||||
self->_pairAlert = nil;
|
||||
[self->_discMan startDiscovery];
|
||||
[self hideLoadingFrame: nil];
|
||||
[self hideLoadingFrame: ^{
|
||||
[self showHostSelectionView];
|
||||
}];
|
||||
}]];
|
||||
[[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)displayFailureDialog:(NSString *)message {
|
||||
- (void)displayPairingFailureDialog:(NSString *)message {
|
||||
UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:failedDialog];
|
||||
[failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
|
||||
[_discMan startDiscovery];
|
||||
|
||||
[self hideLoadingFrame: ^{
|
||||
[self showHostSelectionView];
|
||||
[[self activeViewController] presentViewController:failedDialog animated:YES completion:nil];
|
||||
}];
|
||||
|
||||
[_discMan startDiscovery];
|
||||
}
|
||||
|
||||
- (void)pairFailed:(NSString *)message {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (self->_pairAlert != nil) {
|
||||
[self->_pairAlert dismissViewControllerAnimated:YES completion:^{
|
||||
[self displayFailureDialog:message];
|
||||
[self displayPairingFailureDialog:message];
|
||||
}];
|
||||
self->_pairAlert = nil;
|
||||
}
|
||||
else {
|
||||
[self displayFailureDialog:message];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -121,12 +128,28 @@ static NSMutableSet* hostList;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)updateTitle {
|
||||
if (_selectedHost != nil) {
|
||||
self.title = _selectedHost.name;
|
||||
}
|
||||
else if ([hostList count] == 0) {
|
||||
self.title = @"Searching for PCs on your network...";
|
||||
}
|
||||
else {
|
||||
self.title = @"Select Host";
|
||||
}
|
||||
}
|
||||
|
||||
- (void)alreadyPaired {
|
||||
BOOL usingCachedAppList = false;
|
||||
|
||||
// Capture the host here because it can change once we
|
||||
// leave the main thread
|
||||
TemporaryHost* host = _selectedHost;
|
||||
if (host == nil) {
|
||||
[self hideLoadingFrame: nil];
|
||||
return;
|
||||
}
|
||||
|
||||
if ([host.appList count] > 0) {
|
||||
usingCachedAppList = true;
|
||||
@@ -136,9 +159,6 @@ static NSMutableSet* hostList;
|
||||
return;
|
||||
}
|
||||
|
||||
self.title = host.name;
|
||||
|
||||
[self enableUpButton];
|
||||
[self updateAppsForHost:host];
|
||||
[self hideLoadingFrame: nil];
|
||||
});
|
||||
@@ -146,13 +166,13 @@ static NSMutableSet* hostList;
|
||||
Log(LOG_I, @"Using cached app list: %d", usingCachedAppList);
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Exempt this host from discovery while handling the applist query
|
||||
[self->_discMan removeHostFromDiscovery:host];
|
||||
[self->_discMan pauseDiscoveryForHost:host];
|
||||
|
||||
AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress serverCert:host.serverCert uniqueID:self->_uniqueId];
|
||||
|
||||
[self->_discMan addHostToDiscovery:host];
|
||||
[self->_discMan resumeDiscoveryForHost:host];
|
||||
|
||||
if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
||||
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
||||
Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (host != self->_selectedHost) {
|
||||
@@ -160,16 +180,16 @@ static NSMutableSet* hostList;
|
||||
return;
|
||||
}
|
||||
|
||||
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching App List Failed"
|
||||
message:@"The connection to the PC was interrupted."
|
||||
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Interrupted"
|
||||
message:appListResp.statusMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:applistAlert];
|
||||
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[self hideLoadingFrame: ^{
|
||||
[self showHostSelectionView];
|
||||
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
|
||||
}];
|
||||
host.state = StateOffline;
|
||||
[self showHostSelectionView];
|
||||
});
|
||||
} else {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -180,9 +200,6 @@ static NSMutableSet* hostList;
|
||||
return;
|
||||
}
|
||||
|
||||
self.title = host.name;
|
||||
[self enableUpButton];
|
||||
|
||||
[self updateAppsForHost:host];
|
||||
[self->_appManager stopRetrieving];
|
||||
[self->_appManager retrieveAssetsFromHost:host];
|
||||
@@ -192,22 +209,42 @@ 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];
|
||||
|
||||
for (TemporaryApp* app in newList) {
|
||||
BOOL appAlreadyInList = NO;
|
||||
for (TemporaryApp* savedApp in host.appList) {
|
||||
for (TemporaryApp* savedApp in newHostAppList) {
|
||||
if ([app.id isEqualToString:savedApp.id]) {
|
||||
savedApp.name = app.name;
|
||||
savedApp.hdrSupported = app.hdrSupported;
|
||||
// Don't propagate hidden, because we want the local data to prevail
|
||||
appAlreadyInList = YES;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!appAlreadyInList) {
|
||||
app.host = host;
|
||||
[host.appList addObject:app];
|
||||
[newHostAppList addObject:app];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -215,7 +252,7 @@ static NSMutableSet* hostList;
|
||||
do {
|
||||
appWasRemoved = NO;
|
||||
|
||||
for (TemporaryApp* app in host.appList) {
|
||||
for (TemporaryApp* app in newHostAppList) {
|
||||
appWasRemoved = YES;
|
||||
for (TemporaryApp* mergedApp in newList) {
|
||||
if ([mergedApp.id isEqualToString:app.id]) {
|
||||
@@ -227,7 +264,7 @@ static NSMutableSet* hostList;
|
||||
// Removing the app mutates the list we're iterating (which isn't legal).
|
||||
// We need to jump out of this loop and restart enumeration.
|
||||
|
||||
[host.appList removeObject:app];
|
||||
[newHostAppList removeObject:app];
|
||||
|
||||
// It's important to remove the app record from the database
|
||||
// since we'll have a constraint violation now that appList
|
||||
@@ -240,21 +277,29 @@ static NSMutableSet* hostList;
|
||||
|
||||
// Keep looping until the list is no longer being mutated
|
||||
} while (appWasRemoved);
|
||||
|
||||
host.appList = newHostAppList;
|
||||
|
||||
[database updateAppsForExistingHost:host];
|
||||
|
||||
// This host may be eligible for a shortcut now that the app list
|
||||
// has been populated
|
||||
[self updateHostShortcuts];
|
||||
}
|
||||
|
||||
- (void)showHostSelectionView {
|
||||
#if TARGET_OS_TV
|
||||
// Remove the menu button intercept to allow the app to exit
|
||||
// when at the host selection view.
|
||||
[self.view removeGestureRecognizer:_menuRecognizer];
|
||||
[self.navigationController.view removeGestureRecognizer:_menuRecognizer];
|
||||
#endif
|
||||
|
||||
[_appManager stopRetrieving];
|
||||
_showHiddenApps = NO;
|
||||
_selectedHost = nil;
|
||||
_sortedAppList = nil;
|
||||
|
||||
self.title = @"Select Host";
|
||||
[self updateTitle];
|
||||
[self disableUpButton];
|
||||
|
||||
[self.collectionView reloadData];
|
||||
@@ -292,11 +337,13 @@ static NSMutableSet* hostList;
|
||||
|
||||
Log(LOG_D, @"Clicked host: %@", host.name);
|
||||
_selectedHost = host;
|
||||
[self updateTitle];
|
||||
[self enableUpButton];
|
||||
[self disableNavigation];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
// Intercept the menu key to go back to the host page
|
||||
[self.view addGestureRecognizer:_menuRecognizer];
|
||||
[self.navigationController.view addGestureRecognizer:_menuRecognizer];
|
||||
#endif
|
||||
|
||||
// If we are online, paired, and have a cached app list, skip straight
|
||||
@@ -311,16 +358,31 @@ static NSMutableSet* hostList;
|
||||
|
||||
[self showLoadingFrame: ^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Wait for the PC's status to be known
|
||||
while (host.state == StateUnknown) {
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
// Don't bother polling if the server is already offline
|
||||
if (host.state == StateOffline) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self hideLoadingFrame:^{
|
||||
[self showHostSelectionView];
|
||||
}];
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId serverCert:host.serverCert];
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
|
||||
// Exempt this host from discovery while handling the serverinfo request
|
||||
[self->_discMan removeHostFromDiscovery:host];
|
||||
[self->_discMan pauseDiscoveryForHost:host];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
|
||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
[self->_discMan addHostToDiscovery:host];
|
||||
[self->_discMan resumeDiscoveryForHost:host];
|
||||
|
||||
if (serverInfoResp == nil || ![serverInfoResp isStatusOk]) {
|
||||
if (![serverInfoResp isStatusOk]) {
|
||||
Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
if (host != self->_selectedHost) {
|
||||
@@ -328,8 +390,8 @@ static NSMutableSet* hostList;
|
||||
return;
|
||||
}
|
||||
|
||||
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Fetching Server Info Failed"
|
||||
message:@"The connection to the PC was interrupted."
|
||||
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:serverInfoResp.statusMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:applistAlert];
|
||||
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
@@ -337,13 +399,13 @@ static NSMutableSet* hostList;
|
||||
// Only display an alert if this was the result of a real
|
||||
// user action, not just passively entering the foreground again
|
||||
[self hideLoadingFrame: ^{
|
||||
[self showHostSelectionView];
|
||||
if (view != nil) {
|
||||
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
|
||||
}
|
||||
}];
|
||||
|
||||
host.state = StateOffline;
|
||||
[self showHostSelectionView];
|
||||
});
|
||||
} else {
|
||||
// Update the host object with this data
|
||||
@@ -411,8 +473,8 @@ static NSMutableSet* hostList;
|
||||
|
||||
UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
if (host.state != StateOnline) {
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake On LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake PC" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake-On-LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[wolAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) {
|
||||
wolAlert.message = @"Host MAC unknown, unable to send WOL Packet";
|
||||
@@ -420,17 +482,53 @@ static NSMutableSet* hostList;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
[WakeOnLanManager wakeHost:host];
|
||||
});
|
||||
wolAlert.message = @"Sent WOL Packet";
|
||||
wolAlert.message = @"Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN.";
|
||||
}
|
||||
[[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
|
||||
}]];
|
||||
|
||||
}
|
||||
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];
|
||||
}]];
|
||||
}
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Test Network" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action) {
|
||||
[self showLoadingFrame:^{
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Perform the network test on a GCD worker thread. It may take a while.
|
||||
unsigned int portTestResult = LiTestClientConnectivity(CONN_TEST_SERVER, 443, ML_PORT_FLAG_ALL);
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
[self hideLoadingFrame:^{
|
||||
NSString* message;
|
||||
|
||||
if (portTestResult == 0) {
|
||||
message = @"This network does not appear to be blocking Moonlight. If you still have trouble connecting, check your PC's firewall settings.\n\nVisit the Moonlight Setup Guide on GitHub for additional setup help and troubleshooting steps.";
|
||||
}
|
||||
else if (portTestResult == ML_TEST_RESULT_INCONCLUSIVE) {
|
||||
message = @"The network test could not be performed because none of Moonlight's connection testing servers were reachable. Check your Internet connection or try again later.";
|
||||
}
|
||||
else {
|
||||
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];
|
||||
[netTestAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
[[self activeViewController] presentViewController:netTestAlert animated:YES completion:nil];
|
||||
}];
|
||||
});
|
||||
});
|
||||
}];
|
||||
}]];
|
||||
#if !TARGET_OS_TV
|
||||
if (host.state != StateOnline) {
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
|
||||
}]];
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Remove Host" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
|
||||
[self->_discMan removeHostFromDiscovery:host];
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
@@ -447,35 +545,46 @@ 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 {
|
||||
Log(LOG_D, @"Clicked add host");
|
||||
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Host Address" message:@"Please enter a hostname or IP address" preferredStyle:UIAlertControllerStyleAlert];
|
||||
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:@"If Moonlight doesn't find your local gaming PC automatically,\nenter the IP address of your PC" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
||||
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
NSString* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
|
||||
if (host != nil) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@synchronized(hostList) {
|
||||
[hostList addObject:host];
|
||||
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){
|
||||
if (host != nil) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self hideLoadingFrame:^{
|
||||
@synchronized(hostList) {
|
||||
[hostList addObject:host];
|
||||
}
|
||||
[self updateHosts];
|
||||
}];
|
||||
});
|
||||
} else {
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443,
|
||||
ML_PORT_FLAG_TCP_47984 | ML_PORT_FLAG_TCP_47989);
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
error = [error stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
|
||||
}
|
||||
[self updateHosts];
|
||||
});
|
||||
} else {
|
||||
UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host" message:error preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:hostNotFoundAlert];
|
||||
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
|
||||
});
|
||||
}
|
||||
}];});
|
||||
|
||||
UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host Manually" message:error preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:hostNotFoundAlert];
|
||||
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self hideLoadingFrame:^{
|
||||
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
|
||||
}];
|
||||
});
|
||||
}
|
||||
}];
|
||||
});
|
||||
}];
|
||||
}]];
|
||||
[alertController addTextFieldWithConfigurationHandler:nil];
|
||||
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
|
||||
@@ -492,10 +601,28 @@ static NSMutableSet* hostList;
|
||||
TemporarySettings* streamSettings = [dataMan getSettings];
|
||||
|
||||
_streamConfig.frameRate = [streamSettings.framerate intValue];
|
||||
_streamConfig.bitRate = [streamSettings.bitrate intValue];
|
||||
if (@available(iOS 10.3, *)) {
|
||||
// Don't stream more FPS than the display can show
|
||||
if (_streamConfig.frameRate > [UIScreen mainScreen].maximumFramesPerSecond) {
|
||||
_streamConfig.frameRate = (int)[UIScreen mainScreen].maximumFramesPerSecond;
|
||||
Log(LOG_W, @"Clamping FPS to maximum refresh rate: %d", _streamConfig.frameRate);
|
||||
}
|
||||
}
|
||||
|
||||
_streamConfig.height = [streamSettings.height intValue];
|
||||
_streamConfig.width = [streamSettings.width intValue];
|
||||
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
|
||||
#if TARGET_OS_TV
|
||||
// Don't allow streaming 4K on the Apple TV HD
|
||||
struct utsname systemInfo;
|
||||
uname(&systemInfo);
|
||||
if (strcmp(systemInfo.machine, "AppleTV5,3") == 0 && _streamConfig.height >= 2160) {
|
||||
Log(LOG_W, @"4K streaming not supported on Apple TV HD");
|
||||
_streamConfig.width = 1920;
|
||||
_streamConfig.height = 1080;
|
||||
}
|
||||
#endif
|
||||
|
||||
_streamConfig.bitRate = [streamSettings.bitrate intValue];
|
||||
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
|
||||
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
|
||||
_streamConfig.allowHevc = streamSettings.useHevc;
|
||||
@@ -506,14 +633,16 @@ static NSMutableSet* hostList;
|
||||
|
||||
|
||||
// Probe for supported channel configurations
|
||||
Log(LOG_I, @"Audio device supports %d channels", [AVAudioSession sharedInstance].maximumOutputNumberOfChannels);
|
||||
if ([AVAudioSession sharedInstance].maximumOutputNumberOfChannels >= 6) {
|
||||
_streamConfig.audioChannelCount = 6;
|
||||
_streamConfig.audioChannelMask = 0xFC;
|
||||
long outputChannels = [AVAudioSession sharedInstance].maximumOutputNumberOfChannels;
|
||||
Log(LOG_I, @"Audio device supports %d channels", outputChannels);
|
||||
if (outputChannels >= 8) {
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_71_SURROUND;
|
||||
}
|
||||
else if (outputChannels >= 6) {
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||
}
|
||||
else {
|
||||
_streamConfig.audioChannelCount = 2;
|
||||
_streamConfig.audioChannelMask = 0x3;
|
||||
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||
}
|
||||
|
||||
// HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client.
|
||||
@@ -531,8 +660,8 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
}
|
||||
|
||||
- (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];
|
||||
|
||||
@@ -544,18 +673,43 @@ 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);
|
||||
@@ -566,7 +720,7 @@ static NSMutableSet* hostList;
|
||||
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
|
||||
|
||||
// Exempt this host from discovery while handling the quit operation
|
||||
[self->_discMan removeHostFromDiscovery:app.host];
|
||||
[self->_discMan pauseDiscoveryForHost:app.host];
|
||||
[hMan executeRequestSynchronously:quitRequest];
|
||||
if (quitResponse.statusCode == 200) {
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
@@ -583,7 +737,7 @@ static NSMutableSet* hostList;
|
||||
[serverInfoResp populateHost:app.host];
|
||||
}
|
||||
}
|
||||
[self->_discMan addHostToDiscovery:app.host];
|
||||
[self->_discMan resumeDiscoveryForHost:app.host];
|
||||
|
||||
// If it fails, display an error and stop the current operation
|
||||
if (quitResponse.statusCode != 200) {
|
||||
@@ -602,9 +756,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];
|
||||
@@ -622,8 +773,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];
|
||||
@@ -652,7 +841,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
|
||||
|
||||
@@ -691,9 +880,7 @@ static NSMutableSet* hostList;
|
||||
- (void)viewDidLoad
|
||||
{
|
||||
[super viewDidLoad];
|
||||
|
||||
self.title = @"Select Host";
|
||||
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Set the side bar button action. When it's tapped, it'll show the sidebar.
|
||||
[_settingsButton setTarget:self.revealViewController];
|
||||
@@ -714,6 +901,14 @@ static NSMutableSet* hostList;
|
||||
// if the user drags all the way off the screen opposite the settings pane.
|
||||
self.revealViewController.bounceBackOnOverdraw = NO;
|
||||
#else
|
||||
// The settings button will direct the user into the Settings app on tvOS
|
||||
[_settingsButton setTarget:self];
|
||||
[_settingsButton setAction:@selector(openTvSettings:)];
|
||||
|
||||
// Restore focus on the selected app on view controller pop navigation
|
||||
self.restoresFocusAfterTransition = NO;
|
||||
self.collectionView.remembersLastFocusedIndexPath = YES;
|
||||
|
||||
_menuRecognizer = [[UITapGestureRecognizer alloc] init];
|
||||
[_menuRecognizer addTarget:self action: @selector(showHostSelectionView)];
|
||||
_menuRecognizer.allowedPressTypes = [[NSArray alloc] initWithObjects:[NSNumber numberWithLong:UIPressTypeMenu], nil];
|
||||
@@ -750,34 +945,102 @@ 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];
|
||||
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
|
||||
|
||||
[self.view addSubview:hostScrollView];
|
||||
|
||||
if ([hostList count] == 1) {
|
||||
[self hostClicked:[hostList anyObject] view:nil];
|
||||
}
|
||||
else {
|
||||
[self updateTitle];
|
||||
[self.view addSubview:hostScrollView];
|
||||
}
|
||||
}
|
||||
|
||||
-(void)beginForegroundRefresh:(bool)refreshAppList
|
||||
#if TARGET_OS_TV
|
||||
-(void)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];
|
||||
}
|
||||
#endif
|
||||
|
||||
-(void)beginForegroundRefresh
|
||||
{
|
||||
if (!_background) {
|
||||
// This will kick off box art caching
|
||||
[self updateHosts];
|
||||
|
||||
// Reset state first so we can rediscover hosts that were deleted before
|
||||
[_discMan resetDiscoveryState];
|
||||
[_discMan startDiscovery];
|
||||
|
||||
// This will refresh the applist when a paired host is selected
|
||||
if (refreshAppList && _selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
|
||||
if (_selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
|
||||
[self hostClicked:_selectedHost view:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void)handlePendingShortcutAction
|
||||
{
|
||||
// Check if we have a pending shortcut action
|
||||
AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
|
||||
if (delegate.pcUuidToLoad != nil) {
|
||||
// Find the host it corresponds to
|
||||
TemporaryHost* matchingHost = nil;
|
||||
for (TemporaryHost* host in hostList) {
|
||||
if ([host.uuid isEqualToString:delegate.pcUuidToLoad]) {
|
||||
matchingHost = host;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the pending shortcut action
|
||||
delegate.pcUuidToLoad = nil;
|
||||
|
||||
// Complete the request
|
||||
if (delegate.shortcutCompletionHandler != nil) {
|
||||
delegate.shortcutCompletionHandler(matchingHost != nil);
|
||||
delegate.shortcutCompletionHandler = nil;
|
||||
}
|
||||
|
||||
if (matchingHost != nil && _selectedHost != matchingHost) {
|
||||
// Navigate to the host page
|
||||
[self hostClicked:matchingHost view:nil];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void)handleReturnToForeground
|
||||
{
|
||||
_background = NO;
|
||||
|
||||
[self beginForegroundRefresh: YES];
|
||||
[self beginForegroundRefresh];
|
||||
|
||||
// Check for a pending shortcut action when returning to foreground
|
||||
[self handlePendingShortcutAction];
|
||||
}
|
||||
|
||||
-(void)handleEnterBackground
|
||||
@@ -802,6 +1065,9 @@ static NSMutableSet* hostList;
|
||||
[self.navigationController.navigationBar setShadowImage:fakeImage];
|
||||
[self.navigationController.navigationBar setBackgroundImage:fakeImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
|
||||
|
||||
// Check for a pending shortcut action when appearing
|
||||
[self handlePendingShortcutAction];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(handleReturnToForeground)
|
||||
name: UIApplicationDidBecomeActiveNotification
|
||||
@@ -828,12 +1094,7 @@ static NSMutableSet* hostList;
|
||||
// this view via an error dialog from the stream
|
||||
// view, so we won't get a return to active notification
|
||||
// for that which would normally fire beginForegroundRefresh.
|
||||
//
|
||||
// On tvOS, we'll get a viewWillAppear when returning from the
|
||||
// loading frame which will cause an infinite loop by starting
|
||||
// another loading frame. To avoid this, just don't refresh
|
||||
// if we're coming back from a loading frame view.
|
||||
[self beginForegroundRefresh: !([_loadingFrame isShown] || [_loadingFrame isBeingDismissed])];
|
||||
[self beginForegroundRefresh];
|
||||
}
|
||||
|
||||
- (void)viewDidDisappear:(BOOL)animated
|
||||
@@ -877,19 +1138,46 @@ 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];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateHostShortcuts {
|
||||
#if !TARGET_OS_TV
|
||||
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
|
||||
|
||||
@synchronized (hostList) {
|
||||
for (TemporaryHost* host in hostList) {
|
||||
// Pair state may be unknown if we haven't polled it yet, but the app list
|
||||
// count will persist from paired PCs
|
||||
if ([host.appList count] > 0) {
|
||||
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
|
||||
initWithType:@"PC"
|
||||
localizedTitle:host.name
|
||||
localizedSubtitle:nil
|
||||
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
|
||||
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
|
||||
[quickActions addObject: shortcut];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[UIApplication sharedApplication].shortcutItems = quickActions;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)updateHosts {
|
||||
Log(LOG_I, @"Updating hosts...");
|
||||
[[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
@@ -914,6 +1202,12 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
}
|
||||
|
||||
// Create or delete host shortcuts as needed
|
||||
[self updateHostShortcuts];
|
||||
|
||||
// Update the title in case we now have a PC
|
||||
[self updateTitle];
|
||||
|
||||
prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge];
|
||||
addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2);
|
||||
|
||||
@@ -993,6 +1287,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];
|
||||
}
|
||||
@@ -1011,13 +1315,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
|
||||
@@ -1034,8 +1338,8 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
||||
if (_selectedHost != nil) {
|
||||
return _selectedHost.appList.count;
|
||||
if (_selectedHost != nil && _sortedAppList != nil) {
|
||||
return _sortedAppList.count;
|
||||
}
|
||||
else {
|
||||
return 0;
|
||||
@@ -1067,10 +1371,12 @@ static NSMutableSet* hostList;
|
||||
|
||||
- (void) disableNavigation {
|
||||
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO;
|
||||
self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = NO;
|
||||
}
|
||||
|
||||
- (void) enableNavigation {
|
||||
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES;
|
||||
self.navigationController.navigationBar.topItem.leftBarButtonItem.enabled = YES;
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -14,11 +14,14 @@
|
||||
@property (strong, nonatomic) IBOutlet UISlider *bitrateSlider;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *touchModeSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *hevcSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
|
||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
|
||||
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
|
||||
|
||||
#pragma clang diagnostic push
|
||||
|
||||
@@ -43,7 +43,9 @@ static const int bitrateTable[] = {
|
||||
60000,
|
||||
70000,
|
||||
80000,
|
||||
100000
|
||||
100000,
|
||||
120000,
|
||||
150000,
|
||||
};
|
||||
|
||||
-(int)getSliderValueForBitrate:(NSInteger)bitrate {
|
||||
@@ -191,6 +193,10 @@ static const int bitrateTable[] = {
|
||||
[self.hevcSelector setEnabled:NO];
|
||||
}
|
||||
|
||||
[self.touchModeSelector setSelectedSegmentIndex:currentSettings.absoluteTouchMode ? 1 : 0];
|
||||
[self.touchModeSelector addTarget:self action:@selector(touchModeChanged) forControlEvents:UIControlEventValueChanged];
|
||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlay ? 1 : 0];
|
||||
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
|
||||
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
||||
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||
@@ -200,6 +206,7 @@ static const int bitrateTable[] = {
|
||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||
[self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
|
||||
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
|
||||
[self.onscreenControlSelector setEnabled:!currentSettings.absoluteTouchMode];
|
||||
[self.bitrateSlider setMinimumValue:0];
|
||||
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||
@@ -207,6 +214,11 @@ static const int bitrateTable[] = {
|
||||
[self updateBitrateText];
|
||||
}
|
||||
|
||||
- (void) touchModeChanged {
|
||||
// Disable on-screen controls in absolute touch mode
|
||||
[self.onscreenControlSelector setEnabled:[self.touchModeSelector selectedSegmentIndex] == 0];
|
||||
}
|
||||
|
||||
- (void) newResolutionFpsChosen {
|
||||
NSInteger fps = [self getChosenFrameRate];
|
||||
NSInteger width = [self getChosenStreamWidth];
|
||||
@@ -290,17 +302,22 @@ static const int bitrateTable[] = {
|
||||
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
||||
BOOL useHevc = [self.hevcSelector selectedSegmentIndex] == 1;
|
||||
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
|
||||
BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
|
||||
BOOL statsOverlay = [self.statsOverlaySelector selectedSegmentIndex] == 1;
|
||||
[dataMan saveSettingsWithBitrate:_bitrate
|
||||
framerate:framerate
|
||||
height:height
|
||||
width:width
|
||||
onscreenControls:onscreenControls
|
||||
remote:NO
|
||||
optimizeGames:optimizeGames
|
||||
multiController:multiController
|
||||
audioOnPC:audioOnPC
|
||||
useHevc:useHevc
|
||||
enableHdr:NO];
|
||||
enableHdr:NO
|
||||
btMouseSupport:btMouseSupport
|
||||
absoluteTouchMode:absoluteTouchMode
|
||||
statsOverlay:statsOverlay];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning {
|
||||
|
||||
@@ -15,13 +15,10 @@
|
||||
#if TARGET_OS_TV
|
||||
@import GameController;
|
||||
|
||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, EdgeDetectionDelegate>
|
||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, InputPresenceDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
||||
#else
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, EdgeDetectionDelegate>
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, InputPresenceDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
||||
#endif
|
||||
@property (strong, nonatomic) IBOutlet UILabel *stageLabel;
|
||||
@property (strong, nonatomic) IBOutlet UILabel *tipLabel;
|
||||
@property (strong, nonatomic) IBOutlet UIActivityIndicatorView *spinner;
|
||||
@property (nonatomic) StreamConfiguration* streamConfig;
|
||||
|
||||
@end
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "DataManager.h"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <netinet/in.h>
|
||||
@@ -20,10 +21,24 @@
|
||||
@implementation StreamFrameViewController {
|
||||
ControllerSupport *_controllerSupport;
|
||||
StreamManager *_streamMan;
|
||||
TemporarySettings *_settings;
|
||||
NSTimer *_inactivityTimer;
|
||||
UITapGestureRecognizer *_menuGestureRecognizer;
|
||||
NSTimer *_statsUpdateTimer;
|
||||
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
||||
UITextView *_overlayView;
|
||||
UILabel *_stageLabel;
|
||||
UILabel *_tipLabel;
|
||||
UIActivityIndicatorView *_spinner;
|
||||
StreamView *_streamView;
|
||||
UIScrollView *_scrollView;
|
||||
BOOL _userIsInteracting;
|
||||
CGSize _keyboardSize;
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer;
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
@@ -35,12 +50,16 @@
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef TARGET_OS_TV
|
||||
#if TARGET_OS_TV
|
||||
- (void)controllerPauseButtonPressed:(id)sender { }
|
||||
- (void)controllerPauseButtonDoublePressed:(id)sender {
|
||||
Log(LOG_I, @"Menu double-pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
- (void)controllerPlayPauseButtonPressed:(id)sender {
|
||||
Log(LOG_I, @"Play/Pause button pressed -- backing out of stream");
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -50,44 +69,78 @@
|
||||
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
|
||||
[(StreamView*)self.view setupStreamView];
|
||||
|
||||
[self.stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
|
||||
[self.stageLabel sizeToFit];
|
||||
self.stageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
||||
self.spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - self.stageLabel.frame.size.height - self.spinner.frame.size.height);
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
|
||||
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig];
|
||||
_inactivityTimer = nil;
|
||||
_settings = [[[DataManager alloc] init] getSettings];
|
||||
|
||||
_stageLabel = [[UILabel alloc] init];
|
||||
[_stageLabel setUserInteractionEnabled:NO];
|
||||
[_stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
|
||||
[_stageLabel sizeToFit];
|
||||
_stageLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_stageLabel.textColor = [UIColor whiteColor];
|
||||
_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
||||
|
||||
_spinner = [[UIActivityIndicatorView alloc] init];
|
||||
[_spinner setUserInteractionEnabled:NO];
|
||||
#if TARGET_OS_TV
|
||||
if (!_menuGestureRecognizer || !_menuDoubleTapGestureRecognizer) {
|
||||
_menuGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
||||
_menuGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
||||
#else
|
||||
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
|
||||
#endif
|
||||
[_spinner sizeToFit];
|
||||
[_spinner startAnimating];
|
||||
_spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
|
||||
|
||||
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig presenceDelegate:self];
|
||||
_inactivityTimer = nil;
|
||||
|
||||
_streamView = [[StreamView alloc] initWithFrame:self.view.frame];
|
||||
[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
|
||||
_menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
||||
_menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
|
||||
_playPauseTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPlayPauseButtonPressed:)];
|
||||
_playPauseTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypePlayPause)];
|
||||
|
||||
_menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
|
||||
_menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
|
||||
[_menuGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
|
||||
[_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
|
||||
_menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
||||
}
|
||||
|
||||
[self.view addGestureRecognizer:_menuGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_menuTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
|
||||
[self.view addGestureRecognizer:_playPauseTapGestureRecognizer];
|
||||
|
||||
#else
|
||||
_exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
||||
_exitSwipeRecognizer.edges = UIRectEdgeLeft;
|
||||
_exitSwipeRecognizer.delaysTouchesBegan = NO;
|
||||
_exitSwipeRecognizer.delaysTouchesEnded = NO;
|
||||
|
||||
[self.view addGestureRecognizer:_exitSwipeRecognizer];
|
||||
#endif
|
||||
|
||||
_tipLabel = [[UILabel alloc] init];
|
||||
[_tipLabel setUserInteractionEnabled:NO];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
[self.tipLabel setText:@"Tip: Double tap the Menu button to disconnect from your PC"];
|
||||
[_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"];
|
||||
#else
|
||||
[self.tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
|
||||
[_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
|
||||
#endif
|
||||
|
||||
[self.tipLabel sizeToFit];
|
||||
self.tipLabel.textAlignment = NSTextAlignmentCenter;
|
||||
self.tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
|
||||
[_tipLabel sizeToFit];
|
||||
_tipLabel.textColor = [UIColor whiteColor];
|
||||
_tipLabel.textAlignment = NSTextAlignmentCenter;
|
||||
_tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
|
||||
|
||||
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
|
||||
renderView:self.view
|
||||
renderView:_streamView
|
||||
connectionCallbacks:self];
|
||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||
[opQueue addOperation:_streamMan];
|
||||
@@ -106,6 +159,48 @@
|
||||
selector: @selector(applicationDidEnterBackground:)
|
||||
name: UIApplicationDidEnterBackgroundNotification
|
||||
object: nil];
|
||||
|
||||
#if 0
|
||||
// FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard
|
||||
// several times in a row will not correctly restore the state of the UIScrollView.
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillShow:)
|
||||
name: UIKeyboardWillShowNotification
|
||||
object: nil];
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver: self
|
||||
selector: @selector(keyboardWillHide:)
|
||||
name: UIKeyboardWillHideNotification
|
||||
object: nil];
|
||||
#endif
|
||||
|
||||
// Only enable scroll and zoom in absolute touch mode
|
||||
if (_settings.absoluteTouchMode) {
|
||||
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
|
||||
#if !TARGET_OS_TV
|
||||
[_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2];
|
||||
#endif
|
||||
[_scrollView setShowsHorizontalScrollIndicator:NO];
|
||||
[_scrollView setShowsVerticalScrollIndicator:NO];
|
||||
[_scrollView setDelegate:self];
|
||||
[_scrollView setMaximumZoomScale:10.0f];
|
||||
|
||||
// Add StreamView inside a UIScrollView for absolute mode
|
||||
[_scrollView addSubview:_streamView];
|
||||
[self.view addSubview:_scrollView];
|
||||
}
|
||||
else {
|
||||
// Add StreamView directly in relative mode
|
||||
[self.view addSubview:_streamView];
|
||||
}
|
||||
|
||||
[self.view addSubview:_stageLabel];
|
||||
[self.view addSubview:_spinner];
|
||||
[self.view addSubview:_tipLabel];
|
||||
}
|
||||
|
||||
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
|
||||
return _streamView;
|
||||
}
|
||||
|
||||
- (void)willMoveToParentViewController:(UIViewController *)parent {
|
||||
@@ -122,24 +217,72 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if 0
|
||||
- (void)keyboardWillShow:(NSNotification *)notification {
|
||||
_keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height -= self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
|
||||
-(void)keyboardWillHide:(NSNotification *)notification {
|
||||
// NOTE: UIKeyboardFrameEndUserInfoKey returns a different keyboard size
|
||||
// than UIKeyboardFrameBeginUserInfoKey, so it's unsuitable for use here
|
||||
// to undo the changes made by keyboardWillShow.
|
||||
|
||||
[UIView animateWithDuration:0.3 animations:^{
|
||||
CGRect frame = self->_scrollView.frame;
|
||||
frame.size.height += self->_keyboardSize.height;
|
||||
self->_scrollView.frame = frame;
|
||||
}];
|
||||
}
|
||||
#endif
|
||||
|
||||
- (void)updateStatsOverlay {
|
||||
NSString* overlayText = [self->_streamMan getStatsOverlayText];
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self updateOverlayText:overlayText];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)updateOverlayText:(NSString*)text {
|
||||
if (_overlayView == nil) {
|
||||
_overlayView = [[UITextView alloc] init];
|
||||
#ifndef TARGET_OS_TV
|
||||
#if !TARGET_OS_TV
|
||||
[_overlayView setEditable:NO];
|
||||
#endif
|
||||
[_overlayView setUserInteractionEnabled:NO];
|
||||
[_overlayView setSelectable:NO];
|
||||
[_overlayView setScrollEnabled:NO];
|
||||
[_overlayView setTextAlignment:NSTextAlignmentCenter];
|
||||
[_overlayView setTextColor:[OSColor lightGrayColor]];
|
||||
[_overlayView setBackgroundColor:[OSColor blackColor]];
|
||||
|
||||
// HACK: If not using stats overlay, center the text
|
||||
if (_statsUpdateTimer == nil) {
|
||||
[_overlayView setTextAlignment:NSTextAlignmentCenter];
|
||||
}
|
||||
|
||||
[_overlayView setTextColor:[UIColor lightGrayColor]];
|
||||
[_overlayView setBackgroundColor:[UIColor blackColor]];
|
||||
#if TARGET_OS_TV
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:24]];
|
||||
#else
|
||||
[_overlayView setFont:[UIFont systemFontOfSize:12]];
|
||||
#endif
|
||||
[_overlayView setAlpha:0.5];
|
||||
[self.view addSubview:_overlayView];
|
||||
}
|
||||
|
||||
if (text != nil) {
|
||||
// We set our bounds to the maximum width in order to work around a bug where
|
||||
// sizeToFit interacts badly with the UITextView's line breaks, causing the
|
||||
// width to get smaller and smaller each time as more line breaks are inserted.
|
||||
[_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
|
||||
_overlayView.frame.origin.y,
|
||||
self.view.frame.size.width,
|
||||
_overlayView.frame.size.height)];
|
||||
[_overlayView setText:text];
|
||||
[_overlayView sizeToFit];
|
||||
[_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)];
|
||||
@@ -151,6 +294,9 @@
|
||||
}
|
||||
|
||||
- (void) returnToMainFrame {
|
||||
[_statsUpdateTimer invalidate];
|
||||
_statsUpdateTimer = nil;
|
||||
|
||||
[self.navigationController popToRootViewControllerAnimated:YES];
|
||||
}
|
||||
|
||||
@@ -160,13 +306,15 @@
|
||||
[_inactivityTimer invalidate];
|
||||
}
|
||||
|
||||
// Terminate the stream if the app is inactive for 10 seconds
|
||||
#if !TARGET_OS_TV
|
||||
// Terminate the stream if the app is inactive for 60 seconds
|
||||
Log(LOG_I, @"Starting inactivity termination timer");
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:10
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
|
||||
target:self
|
||||
selector:@selector(inactiveTimerExpired:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)inactiveTimerExpired:(NSTimer*)timer {
|
||||
@@ -209,29 +357,80 @@
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Leave the spinner spinning until it's obscured by
|
||||
// the first frame of video.
|
||||
self.stageLabel.hidden = YES;
|
||||
self.tipLabel.hidden = YES;
|
||||
#if !TARGET_OS_TV
|
||||
[(StreamView*)self.view setupOnScreenControls: self->_controllerSupport swipeDelegate:self];
|
||||
#endif
|
||||
self->_stageLabel.hidden = YES;
|
||||
self->_tipLabel.hidden = YES;
|
||||
|
||||
[self->_streamView showOnScreenControls];
|
||||
|
||||
if (self->_settings.statsOverlay) {
|
||||
self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
|
||||
target:self
|
||||
selector:@selector(updateStatsOverlay)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)connectionTerminated:(long)errorCode {
|
||||
Log(LOG_I, @"Connection terminated: %ld", errorCode);
|
||||
- (void)connectionTerminated:(int)errorCode {
|
||||
Log(LOG_I, @"Connection terminated: %d", errorCode);
|
||||
|
||||
unsigned int portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
if (errorCode == 0) {
|
||||
[self returnToMainFrame];
|
||||
} else {
|
||||
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:@"Connection Terminated" message:@"The connection was terminated" preferredStyle:UIAlertControllerStyleAlert];
|
||||
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:conTermAlert animated:YES completion:nil];
|
||||
|
||||
NSString* title;
|
||||
NSString* message;
|
||||
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
title = @"Connection Error";
|
||||
message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
|
||||
}
|
||||
else {
|
||||
switch (errorCode) {
|
||||
case ML_ERROR_GRACEFUL_TERMINATION:
|
||||
[self returnToMainFrame];
|
||||
return;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_TRAFFIC:
|
||||
title = @"Connection Error";
|
||||
message = @"No video received from host.";
|
||||
if (portFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
break;
|
||||
|
||||
case ML_ERROR_NO_VIDEO_FRAME:
|
||||
title = @"Connection Error";
|
||||
message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
|
||||
break;
|
||||
|
||||
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
||||
case ML_ERROR_PROTECTED_CONTENT:
|
||||
title = @"Connection Error";
|
||||
message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
|
||||
break;
|
||||
|
||||
default:
|
||||
title = @"Connection Terminated";
|
||||
message = @"The connection was terminated";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:conTermAlert];
|
||||
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
[self returnToMainFrame];
|
||||
}]];
|
||||
[self presentViewController:conTermAlert animated:YES completion:nil];
|
||||
});
|
||||
|
||||
[_streamMan stopStream];
|
||||
@@ -242,25 +441,36 @@
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
|
||||
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
|
||||
[self.stageLabel setText:titleCase];
|
||||
[self.stageLabel sizeToFit];
|
||||
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
|
||||
[self->_stageLabel setText:titleCase];
|
||||
[self->_stageLabel sizeToFit];
|
||||
self->_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self->_stageLabel.center.y);
|
||||
});
|
||||
}
|
||||
|
||||
- (void) stageComplete:(const char*)stageName {
|
||||
}
|
||||
|
||||
- (void) stageFailed:(const char*)stageName withError:(long)errorCode {
|
||||
Log(LOG_I, @"Stage %s failed: %ld", stageName, errorCode);
|
||||
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
|
||||
Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
|
||||
|
||||
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
|
||||
if (portTestFlags != 0) {
|
||||
char failingPorts[256];
|
||||
LiStringifyPortFlags(portTestFlags, "\n", failingPorts, sizeof(failingPorts));
|
||||
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
||||
}
|
||||
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
|
||||
message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
|
||||
}
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
message:[NSString stringWithFormat:@"%s failed with error %ld",
|
||||
stageName, errorCode]
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
@@ -279,7 +489,7 @@
|
||||
// Allow the display to go to sleep now
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
||||
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
|
||||
message:message
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:alert];
|
||||
@@ -299,6 +509,11 @@
|
||||
- (void)connectionStatusUpdate:(int)status {
|
||||
Log(LOG_W, @"Connection status update: %d", status);
|
||||
|
||||
// The stats overlay takes precedence over these warnings
|
||||
if (_statsUpdateTimer != nil) {
|
||||
return;
|
||||
}
|
||||
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
switch (status) {
|
||||
case CONN_STATUS_OKAY:
|
||||
@@ -317,21 +532,89 @@
|
||||
});
|
||||
}
|
||||
|
||||
- (void) videoContentShown {
|
||||
[_spinner stopAnimating];
|
||||
[self.view setBackgroundColor:[UIColor blackColor]];
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
{
|
||||
[super didReceiveMemoryWarning];
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
- (void)gamepadPresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)mousePresenceChanged {
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 14.0, *)) {
|
||||
[self setNeedsUpdateOfPrefersPointerLocked];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)userInteractionBegan {
|
||||
// Disable hiding home bar when user is interacting.
|
||||
// iOS will force it to be shown anyway, but it will
|
||||
// also discard our edges deferring system gestures unless
|
||||
// we willingly give up home bar hiding preference.
|
||||
_userIsInteracting = YES;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void)userInteractionEnded {
|
||||
// Enable home bar hiding again if conditions allow
|
||||
_userIsInteracting = NO;
|
||||
#if !TARGET_OS_TV
|
||||
if (@available(iOS 11.0, *)) {
|
||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
// Require a confirmation when streaming to activate a system gesture
|
||||
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
|
||||
return UIRectEdgeAll;
|
||||
}
|
||||
|
||||
- (BOOL)prefersHomeIndicatorAutoHidden {
|
||||
if ([_controllerSupport getConnectedGamepadCount] > 0 &&
|
||||
[_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
|
||||
_userIsInteracting == NO) {
|
||||
// Autohide the home bar when a gamepad is connected
|
||||
// and the on-screen controls are disabled. We can't
|
||||
// do this all the time because any touch on the display
|
||||
// will cause the home indicator to reappear, and our
|
||||
// preferredScreenEdgesDeferringSystemGestures will also
|
||||
// be suppressed (leading to possible errant exits of the
|
||||
// stream).
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)shouldAutorotate {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (BOOL)prefersPointerLocked {
|
||||
// Pointer lock breaks the UIKit mouse APIs, which is a problem because
|
||||
// GCMouse is horribly broken on iOS 14.0 for certain mice. Only lock
|
||||
// the cursor if there is a GCMouse present.
|
||||
return [GCMouse mice].count > 0;
|
||||
}
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -10,9 +10,13 @@
|
||||
|
||||
#import "AppDelegate.h"
|
||||
|
||||
#define SDL_MAIN_HANDLED
|
||||
#import <SDL.h>
|
||||
|
||||
int main(int argc, char * argv[])
|
||||
{
|
||||
@autoreleasepool {
|
||||
SDL_SetMainReady();
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
|
||||
}
|
||||
}
|
||||
|
||||
-1157
File diff suppressed because it is too large
Load Diff
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 6.3 KiB After Width: | Height: | Size: 8.3 KiB |
BIN
Binary file not shown.
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 21 KiB |
@@ -3,6 +3,7 @@
|
||||
{
|
||||
"orientation" : "landscape",
|
||||
"idiom" : "tv",
|
||||
"filename" : "Simulator Screen Shot - Apple TV 4K - 2019-09-22 at 20.39.12.png",
|
||||
"extent" : "full-screen",
|
||||
"minimum-system-version" : "11.0",
|
||||
"scale" : "2x"
|
||||
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 5.0 MiB |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user