Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15fe5a06c2 | ||
|
|
8d4c7bd705 | ||
|
|
b0d85abfa6 | ||
|
|
4295017324 | ||
|
|
d80d70215a | ||
|
|
a46f905aa6 | ||
|
|
453c50d505 | ||
|
|
61c7d9f2b3 | ||
|
|
34707e7743 | ||
|
|
9a9b9651a1 | ||
|
|
d4576a8739 | ||
|
|
87824c2650 | ||
|
|
283e735f90 | ||
|
|
202f968b98 | ||
|
|
62df1cd5ba | ||
|
|
f24c28c848 | ||
|
|
8fb465d3ca | ||
|
|
29dff2320b | ||
|
|
a40b30016b | ||
|
|
409b262a4f | ||
|
|
73197e4378 | ||
|
|
f51dbe4295 | ||
|
|
597aaa1bab | ||
|
|
b9656da8a5 | ||
|
|
422db1898c | ||
|
|
56691791b8 | ||
|
|
de2a5d3cd7 | ||
|
|
49fc10191c | ||
|
|
bf8aaa8198 | ||
|
|
9a4cdc9c94 | ||
|
|
f6153fe818 | ||
|
|
f2fa9e18ea | ||
|
|
aa013be6e6 | ||
|
|
7e21b131ac | ||
|
|
97681f3a0d | ||
|
|
e476cc407c | ||
|
|
f2e03bbf1f | ||
|
|
a6cef32c9a | ||
|
|
6d94897c21 | ||
|
|
a065c78b5f | ||
|
|
83479719a4 | ||
|
|
fd4ede58ca | ||
|
|
7ed5a9831f | ||
|
|
42b7fd35ce | ||
|
|
42c97eafaa | ||
|
|
5b82be0546 | ||
|
|
1529058f52 | ||
|
|
3dcca3a922 | ||
|
|
9b8f230c46 | ||
|
|
7699d9e574 | ||
|
|
641e1c5066 | ||
|
|
617ef7c6a6 | ||
|
|
c2620b8321 | ||
|
|
9e91d136bf | ||
|
|
9960d2a2ff | ||
|
|
311e77e01a | ||
|
|
c08df921da | ||
|
|
ba4e55aac4 | ||
|
|
af16f03703 | ||
|
|
8189d54703 | ||
|
|
ac8514e3cb | ||
|
|
8f4b8da3ce | ||
|
|
8337b3e708 | ||
|
|
cb527baead | ||
|
|
ed10612e2c | ||
|
|
7d3eaeef83 | ||
|
|
dd5ecb37c4 | ||
|
|
636143ee97 | ||
|
|
28ff29bb96 | ||
|
|
a508c6d855 | ||
|
|
dbd6eabd24 | ||
|
|
ad0a5935f7 | ||
|
|
dcd2dc97a6 | ||
|
|
f92544c7d2 | ||
|
|
797fa0f492 | ||
|
|
ff52adf4f8 | ||
|
|
8bb49ada11 | ||
|
|
3f709b616b | ||
|
|
83694f8141 | ||
|
|
a4352aae4d | ||
|
|
2ac19336d6 | ||
|
|
63b09c5b27 | ||
|
|
b2c2f0837a | ||
|
|
335b5aef1f | ||
|
|
918e1248f2 | ||
|
|
442a791e0c | ||
|
|
573b93329d | ||
|
|
40359c5e32 | ||
|
|
3b01b50503 | ||
|
|
76b55cc230 | ||
|
|
e136cd96c1 | ||
|
|
5028eaed59 | ||
|
|
d18c713684 | ||
|
|
08e169eb23 | ||
|
|
611358e35c | ||
|
|
63275130bb | ||
|
|
7c9313e04d | ||
|
|
d466a0af07 | ||
|
|
c8adc184c9 | ||
|
|
d40a29df71 | ||
|
|
85820bf812 | ||
|
|
5d4d10ac8a | ||
|
|
eca018ad15 | ||
|
|
90e582a263 | ||
|
|
3717228e28 | ||
|
|
8c0997ddcd | ||
|
|
d722c9e18d | ||
|
|
94c9433b99 | ||
|
|
befef62b2f | ||
|
|
dc00c1e3e7 | ||
|
|
3555d60a9b | ||
|
|
bd8b9f8bfb | ||
|
|
72dcd76e37 | ||
|
|
fbae7f88b5 | ||
|
|
791a5b1ea1 | ||
|
|
8c5a82573b | ||
|
|
cc56a36bbd | ||
|
|
336dbeed98 | ||
|
|
f8613cdbfd | ||
|
|
a5961dd9b5 | ||
|
|
17a5866fdd | ||
|
|
5e77380636 | ||
|
|
845af246b5 | ||
|
|
87cac4e562 | ||
|
|
70eb4fd69d | ||
|
|
c5120bec37 | ||
|
|
24fc9ac0a9 | ||
|
|
2c0624f1e7 | ||
|
|
8b227f28f7 | ||
|
|
23e9c3f236 | ||
|
|
eefde316de | ||
|
|
a3cfd787de | ||
|
|
410d0d367b | ||
|
|
39b04d46a4 | ||
|
|
0e8647ad07 | ||
|
|
73091e7e8c | ||
|
|
16a2ab3941 | ||
|
|
39a2e8be78 | ||
|
|
8e6dadfa79 | ||
|
|
c2134585df | ||
|
|
dedcbd9d3e | ||
|
|
bd379677a2 | ||
|
|
466ebe2840 | ||
|
|
736887b945 | ||
|
|
993bfca0ae | ||
|
|
5ad87f7400 | ||
|
|
e862dbd6b6 | ||
|
|
befde5eb28 | ||
|
|
7fd3b8df1c | ||
|
|
cee501d9b4 | ||
|
|
a103c0fd45 | ||
|
|
bf71ae003f | ||
|
|
5b102ce3d3 | ||
|
|
c2012fa195 | ||
|
|
003fc8c1f8 | ||
|
|
9123033bb3 | ||
|
|
253144188c | ||
|
|
4ccb9bebbf | ||
|
|
49f647748c | ||
|
|
e2e8f0121d | ||
|
|
b5ea4c3e50 | ||
|
|
90d47004e7 | ||
|
|
450960eaaf | ||
|
|
44550baaa9 | ||
|
|
293dc26b2e | ||
|
|
109fa6f463 | ||
|
|
44f64e1a35 | ||
|
|
2e479837ed | ||
|
|
1b681a8730 | ||
|
|
f988ce0278 | ||
|
|
1ef5c87afc | ||
|
|
f5b99b6b9c | ||
|
|
485dd4b001 | ||
|
|
92e2c0d7cc | ||
|
|
48ffca57ec | ||
|
|
e83a996cd5 | ||
|
|
57191e3b9a | ||
|
|
eadcf3b3eb | ||
|
|
71e0646659 | ||
|
|
f2edee1669 | ||
|
|
11948bf618 | ||
|
|
4a9294a4b7 | ||
|
|
a9b8f753a2 | ||
|
|
05761c787c | ||
|
|
9a7b3b1325 | ||
|
|
b04a4e72ff | ||
|
|
b0b9a614f3 | ||
|
|
76ab786e94 | ||
|
|
8afcdc92d4 | ||
|
|
907a2cd59b | ||
|
|
a85b931d87 | ||
|
|
62f1b3e595 | ||
|
|
b9381ad027 | ||
|
|
98d01f7775 | ||
|
|
de39c87185 | ||
|
|
10a00b0d28 | ||
|
|
4d8788add3 | ||
|
|
9c019be322 | ||
|
|
80254513c4 | ||
|
|
5a13e44fdb | ||
|
|
ec0a462092 | ||
|
|
25c7b18147 | ||
|
|
fb238e0d1d | ||
|
|
bfc0b89926 | ||
|
|
2f0e219726 | ||
|
|
2305f33572 | ||
|
|
d17f2f9dee | ||
|
|
a28049a58c | ||
|
|
2d2f71dcab | ||
|
|
59506a2364 | ||
|
|
2cec53ab6b | ||
|
|
42ef67d939 | ||
|
|
30ec68cd4e | ||
|
|
941c414930 | ||
|
|
b918005555 | ||
|
|
c74fa4d5b0 | ||
|
|
1a71e040c0 | ||
|
|
236c9d5c6e | ||
|
|
862b8632e3 | ||
|
|
cdb5f51ab8 | ||
|
|
314f32388a | ||
|
|
39b0b429d6 | ||
|
|
23a12ed101 | ||
|
|
653a789110 | ||
|
|
04b2fccaef | ||
|
|
ec0124e255 | ||
|
|
132a1fc410 | ||
|
|
63297c8483 | ||
|
|
5fd2c29a73 | ||
|
|
e6e7ec9202 | ||
|
|
fcc770d820 | ||
|
|
4243bcd5d1 | ||
|
|
67b08cd8b9 | ||
|
|
e697938afc | ||
|
|
275f6df8c1 | ||
|
|
aea2067e1c | ||
|
|
b5241a0c30 | ||
|
|
6cac2348b4 | ||
|
|
0293df7748 | ||
|
|
391872ddb8 | ||
|
|
4bafefbe16 | ||
|
|
812f3f3717 | ||
|
|
e498736af6 | ||
|
|
1842a11f56 | ||
|
|
85f81ca8c1 | ||
|
|
b792629943 | ||
|
|
7df76fcd93 | ||
|
|
8b60e79406 | ||
|
|
fcb6c7386c | ||
|
|
2729fad47c | ||
|
|
41eb933e11 | ||
|
|
f759f719e6 | ||
|
|
74283a6763 | ||
|
|
0a19c5950a | ||
|
|
e8832ed746 | ||
|
|
6cc165b589 | ||
|
|
1c86c4485d | ||
|
|
127e5ed54a | ||
|
|
3fee5d0a62 | ||
|
|
c913b3f04f | ||
|
|
dfd30c0624 | ||
|
|
5117a6ede5 | ||
|
|
fb9c071243 | ||
|
|
d0a8f0e0f1 | ||
|
|
6bb6c40bec | ||
|
|
af3bc82d0f | ||
|
|
b9d5d94d70 | ||
|
|
43f4804194 | ||
|
|
be26ce9a8f | ||
|
|
ca562e2251 | ||
|
|
d7ee0916e9 | ||
|
|
3bc9e050f1 | ||
|
|
4b9bd8398b | ||
|
|
20a61dd9f4 | ||
|
|
dc4ecbf262 | ||
|
|
68f55235ec | ||
|
|
5434b653c7 | ||
|
|
25d7af9453 |
@@ -0,0 +1,8 @@
|
||||
# ProBot No Response (https://probot.github.io/apps/no-response/)
|
||||
|
||||
daysUntilClose: 7
|
||||
responseRequiredLabel: 'need more info'
|
||||
closeComment: >
|
||||
This issue has been automatically closed because there was no response to a
|
||||
request for more information from the issue opener. Please leave a comment or
|
||||
open a new issue if you have additional information related to this issue.
|
||||
@@ -0,0 +1,14 @@
|
||||
# ProBot Stale (https://probot.github.io/apps/stale/)
|
||||
|
||||
daysUntilStale: 90
|
||||
daysUntilClose: 7
|
||||
exemptLabels:
|
||||
- accepted
|
||||
- bug
|
||||
- enhancement
|
||||
- meta
|
||||
staleLabel: stale
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs.
|
||||
closeComment: false
|
||||
@@ -0,0 +1,25 @@
|
||||
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
|
||||
@@ -1,19 +1,19 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="8191" systemVersion="15A279b" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES">
|
||||
<?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">
|
||||
<device id="retina6_1" orientation="landscape" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="8154"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14824"/>
|
||||
<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">
|
||||
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" id="y2P-aE-Haa">
|
||||
<rect key="frame" x="0.0" y="0.0" width="320" height="44"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="896" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<animations/>
|
||||
<color key="barTintColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<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">
|
||||
@@ -22,30 +22,14 @@
|
||||
<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"/>
|
||||
<animations/>
|
||||
<color key="backgroundColor" white="0.33333333333333331" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<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">
|
||||
<barButtonItem key="leftBarButtonItem" enabled="NO" id="d9R-Zo-Uml">
|
||||
<button key="customView" opaque="NO" contentMode="center" contentHorizontalAlignment="center" contentVerticalAlignment="center" adjustsImageWhenDisabled="NO" lineBreakMode="middleTruncation" id="9JC-B5-BAE">
|
||||
<rect key="frame" x="20" y="-1" width="109" height="46"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<animations/>
|
||||
<fontDescription key="fontDescription" type="system" pointSize="16"/>
|
||||
<state key="normal" title="Moonlight" image="Logo">
|
||||
<color key="titleColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
<color key="titleShadowColor" white="0.66666666666666663" alpha="1" colorSpace="calibratedWhite"/>
|
||||
</state>
|
||||
<variation key="widthClass=regular" preservesSuperviewLayoutMargins="NO"/>
|
||||
</button>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<navigationItem key="navigationItem" id="rW0-BH-s7l"/>
|
||||
</viewController>
|
||||
</viewControllers>
|
||||
<point key="canvasLocation" x="34" y="149"/>
|
||||
</navigationController>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="Logo" width="32" height="32"/>
|
||||
</resources>
|
||||
</document>
|
||||
|
||||
@@ -6,11 +6,19 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||
|
||||
@property (strong, nonatomic) UIWindow *window;
|
||||
#else
|
||||
#import <Cocoa/Cocoa.h>
|
||||
@interface AppDelegate : NSObject <NSApplicationDelegate>
|
||||
|
||||
@property (readonly, strong) NSPersistentContainer *persistentContainer;
|
||||
@property (strong, nonatomic) NSWindow *window;
|
||||
#endif
|
||||
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectContext *managedObjectContext;
|
||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||
|
||||
@@ -16,35 +16,17 @@
|
||||
|
||||
static NSOperationQueue* mainQueue;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
static NSString* DB_NAME = @"Moonlight_tvOS.bin";
|
||||
#elif TARGET_OS_IPHONE
|
||||
static NSString* DB_NAME = @"Limelight_iOS.sqlite";
|
||||
#else
|
||||
static NSString* DB_NAME = @"moonlight_mac.sqlite";
|
||||
#endif
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
|
||||
{
|
||||
[[UILabel appearance] setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
|
||||
[[UIButton appearance].titleLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
|
||||
|
||||
|
||||
// Generate selected segment background image
|
||||
CGSize borderImageSize = CGSizeMake(1.f, 100.f);
|
||||
|
||||
UIGraphicsBeginImageContext(borderImageSize);
|
||||
CGContextRef context = UIGraphicsGetCurrentContext();
|
||||
|
||||
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
|
||||
CGContextFillRect(context, CGRectMake(0.f, borderImageSize.height * 0.8, borderImageSize.width, borderImageSize.height));
|
||||
|
||||
UIImage *selectedSegmentBG = UIGraphicsGetImageFromCurrentImageContext();
|
||||
UIGraphicsEndImageContext();
|
||||
|
||||
// Clear default border and background color
|
||||
[[UISegmentedControl appearance] setBackgroundImage:[[UIImage alloc] init] forState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
[[UISegmentedControl appearance] setDividerImage:[[UIImage alloc] init] forLeftSegmentState:UIControlStateNormal rightSegmentState:UIControlStateNormal barMetrics:UIBarMetricsDefault];
|
||||
|
||||
// Set selected segment background image
|
||||
[[UISegmentedControl appearance] setBackgroundImage:[selectedSegmentBG imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate] forState:UIControlStateSelected barMetrics:UIBarMetricsDefault];
|
||||
|
||||
// Change font on UISegmentedControl
|
||||
[[UISegmentedControl appearance] setTitleTextAttributes:[NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[UIColor whiteColor], NSForegroundColorAttributeName,
|
||||
[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]], NSFontAttributeName, nil] forState:UIControlStateNormal];
|
||||
|
||||
return YES;
|
||||
}
|
||||
@@ -57,7 +39,7 @@ static NSOperationQueue* mainQueue;
|
||||
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application
|
||||
{
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
|
||||
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
|
||||
}
|
||||
|
||||
@@ -76,15 +58,36 @@ static NSOperationQueue* mainQueue;
|
||||
// Saves changes in the application's managed object context before the application terminates.
|
||||
[self saveContext];
|
||||
}
|
||||
#else
|
||||
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
|
||||
// Insert code here to initialize your application
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
// Insert code here to tear down your application
|
||||
[self saveContext];
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
- (void)saveContext
|
||||
{
|
||||
NSError *error = nil;
|
||||
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
|
||||
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
|
||||
if (managedObjectContext != nil) {
|
||||
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
|
||||
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
||||
}
|
||||
[managedObjectContext performBlock:^{
|
||||
if (![managedObjectContext hasChanges]) {
|
||||
return;
|
||||
}
|
||||
NSError *error = nil;
|
||||
if (![managedObjectContext save:&error]) {
|
||||
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
||||
}
|
||||
|
||||
#if TARGET_OS_TV
|
||||
NSData* dbData = [NSData dataWithContentsOfURL:[[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME]];
|
||||
[[NSUserDefaults standardUserDefaults] setObject:dbData forKey:DB_NAME];
|
||||
#endif
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,19 +128,30 @@ static NSOperationQueue* mainQueue;
|
||||
return _persistentStoreCoordinator;
|
||||
}
|
||||
|
||||
NSURL *storeURL = [self getStoreURL];
|
||||
|
||||
NSError *error = nil;
|
||||
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
|
||||
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
|
||||
if (![_persistentStoreCoordinator addPersistentStoreWithType:NSSQLiteStoreType configuration:nil URL:storeURL options:options error:&error]) {
|
||||
NSString* storeType;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
// Use a binary store for tvOS since we will need exclusive access to the file
|
||||
// to serialize into NSUserDefaults.
|
||||
storeType = NSBinaryStoreType;
|
||||
#else
|
||||
storeType = NSSQLiteStoreType;
|
||||
#endif
|
||||
|
||||
// We must ensure the persistent store is ready to opened
|
||||
[self preparePersistentStore];
|
||||
|
||||
if (![_persistentStoreCoordinator addPersistentStoreWithType:storeType configuration:nil URL:[self getStoreURL] options:options error:&error]) {
|
||||
// Log the error
|
||||
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
||||
|
||||
// Drop the database
|
||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
|
||||
[self dropDatabase];
|
||||
|
||||
// Try again
|
||||
return [self persistentStoreCoordinator];
|
||||
@@ -154,8 +168,50 @@ static NSOperationQueue* mainQueue;
|
||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
||||
}
|
||||
|
||||
- (void) dropDatabase
|
||||
{
|
||||
// Delete the file on disk
|
||||
[[NSFileManager defaultManager] removeItemAtURL:[self getStoreURL] error:nil];
|
||||
|
||||
#if TARGET_OS_TV
|
||||
// Also delete the copy in the NSUserDefaults on tvOS
|
||||
[[NSUserDefaults standardUserDefaults] removeObjectForKey:DB_NAME];
|
||||
#endif
|
||||
}
|
||||
|
||||
- (void) preparePersistentStore
|
||||
{
|
||||
#if TARGET_OS_TV
|
||||
// On tvOS, we may need to inflate the DB from NSUserDefaults
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *cacheDirectory = [paths objectAtIndex:0];
|
||||
NSString *dbPath = [cacheDirectory stringByAppendingPathComponent:DB_NAME];
|
||||
|
||||
// Always prefer the on disk version
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:dbPath]) {
|
||||
// If that is unavailable, inflate it from NSUserDefaults
|
||||
NSData* data = [[NSUserDefaults standardUserDefaults] dataForKey:DB_NAME];
|
||||
if (data != nil) {
|
||||
Log(LOG_I, @"Inflating database from NSUserDefaults");
|
||||
[data writeToFile:dbPath atomically:YES];
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"No database on disk or in NSUserDefaults");
|
||||
}
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"Using cached database");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
- (NSURL*) getStoreURL {
|
||||
return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:@"Limelight_iOS.sqlite"];
|
||||
#if TARGET_OS_TV
|
||||
// We use the cache folder to store our database on tvOS
|
||||
return [[[[NSFileManager defaultManager] URLsForDirectory:NSCachesDirectory inDomains:NSUserDomainMask] lastObject] URLByAppendingPathComponent:DB_NAME];
|
||||
#else
|
||||
return [[self applicationDocumentsDirectory] URLByAppendingPathComponent:DB_NAME];
|
||||
#endif
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,16 +6,14 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface CryptoManager : NSObject
|
||||
|
||||
+ (void) generateKeyPairUsingSSl;
|
||||
+ (void) generateKeyPairUsingSSL;
|
||||
+ (NSData*) readCertFromFile;
|
||||
+ (NSData*) readKeyFromFile;
|
||||
+ (NSData*) readP12FromFile;
|
||||
+ (NSData*) getSignatureFromCert:(NSData*)cert;
|
||||
+ (NSData*) nullTerminateString:(NSData*)data;
|
||||
+ (NSData*) pemToDer:(NSData*)pemCertBytes;
|
||||
|
||||
- (NSData*) createAESKeyFromSaltSHA1:(NSData*)saltedPIN;
|
||||
- (NSData*) createAESKeyFromSaltSHA256:(NSData*)saltedPIN;
|
||||
|
||||
@@ -87,17 +87,28 @@ static NSData* p12 = nil;
|
||||
return (((int)[data length] + 15) / 16) * 16;
|
||||
}
|
||||
|
||||
+ (NSData*) nullTerminateString:(NSData*)data {
|
||||
NSMutableData* mutData = [NSMutableData dataWithData:data];
|
||||
[mutData appendBytes:"" length:1];
|
||||
return mutData;
|
||||
+ (NSData*) pemToDer:(NSData*)pemCertBytes {
|
||||
X509* x509;
|
||||
|
||||
BIO* bio = BIO_new_mem_buf([pemCertBytes bytes], (int)[pemCertBytes length]);
|
||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
BIO_free(bio);
|
||||
|
||||
bio = BIO_new(BIO_s_mem());
|
||||
i2d_X509_bio(bio, x509);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
|
||||
NSData* ret = [[NSData alloc] initWithBytes:mem->data length:mem->length];
|
||||
BIO_free(bio);
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert {
|
||||
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
|
||||
X509* x509;
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
BIO_puts(bio, buffer);
|
||||
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
|
||||
BIO_free(bio);
|
||||
@@ -122,9 +133,7 @@ static NSData* p12 = nil;
|
||||
}
|
||||
|
||||
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
|
||||
const char* buffer = [[CryptoManager nullTerminateString:key] bytes];
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
BIO_puts(bio, buffer);
|
||||
BIO* bio = BIO_new_mem_buf([key bytes], (int)[key length]);
|
||||
|
||||
EVP_PKEY* pkey;
|
||||
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
||||
@@ -159,57 +168,60 @@ static NSData* p12 = nil;
|
||||
return signedData;
|
||||
}
|
||||
|
||||
// TODO: these three methods are almost identical, fix the copy-pasta
|
||||
+ (NSData*) readCryptoObject:(NSString*)item {
|
||||
#if TARGET_OS_TV
|
||||
return [[NSUserDefaults standardUserDefaults] dataForKey:item];
|
||||
#else
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
|
||||
return [NSData dataWithContentsOfFile:file];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (void) writeCryptoObject:(NSString*)item data:(NSData*)data {
|
||||
#if TARGET_OS_TV
|
||||
[[NSUserDefaults standardUserDefaults] setObject:data forKey:item];
|
||||
#else
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *file = [documentsDirectory stringByAppendingPathComponent:item];
|
||||
[data writeToFile:file atomically:NO];
|
||||
#endif
|
||||
}
|
||||
|
||||
+ (NSData*) readCertFromFile {
|
||||
if (cert == nil) {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
|
||||
cert = [NSData dataWithContentsOfFile:certFile];
|
||||
cert = [CryptoManager readCryptoObject:@"client.crt"];
|
||||
}
|
||||
return cert;
|
||||
}
|
||||
|
||||
+ (NSData*) readP12FromFile {
|
||||
if (p12 == nil) {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
|
||||
p12 = [NSData dataWithContentsOfFile:p12File];
|
||||
p12 = [CryptoManager readCryptoObject:@"client.p12"];
|
||||
}
|
||||
return p12;
|
||||
}
|
||||
|
||||
+ (NSData*) readKeyFromFile {
|
||||
if (key == nil) {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
||||
key = [NSData dataWithContentsOfFile:keyFile];
|
||||
key = [CryptoManager readCryptoObject:@"client.key"];
|
||||
}
|
||||
return key;
|
||||
}
|
||||
|
||||
+ (bool) keyPairExists {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
||||
NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
|
||||
NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
|
||||
|
||||
bool keyFileExists = [[NSFileManager defaultManager] fileExistsAtPath:keyFile];
|
||||
bool p12FileExists = [[NSFileManager defaultManager] fileExistsAtPath:p12File];
|
||||
bool certFileExists = [[NSFileManager defaultManager] fileExistsAtPath:certFile];
|
||||
bool keyFileExists = [CryptoManager readCryptoObject:@"client.key"] != nil;
|
||||
bool p12FileExists = [CryptoManager readCryptoObject:@"client.p12"] != nil;
|
||||
bool certFileExists = [CryptoManager readCryptoObject:@"client.crt"] != nil;
|
||||
|
||||
return keyFileExists && p12FileExists && certFileExists;
|
||||
}
|
||||
|
||||
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
||||
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
|
||||
X509* x509;
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
BIO_puts(bio, buffer);
|
||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||
|
||||
if (!x509) {
|
||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
||||
@@ -218,23 +230,59 @@ static NSData* p12 = nil;
|
||||
return [NSData dataWithBytes:x509->signature->data length:x509->signature->length];
|
||||
}
|
||||
|
||||
+ (void) generateKeyPairUsingSSl {
|
||||
+ (NSData*)getKeyFromCertKeyPair:(CertKeyPair*)certKeyPair {
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
|
||||
PEM_write_bio_PrivateKey(bio, certKeyPair->pkey, NULL, NULL, 0, NULL, NULL);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
|
||||
BIO_free(bio);
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (NSData*)getP12FromCertKeyPair:(CertKeyPair*)certKeyPair {
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
|
||||
i2d_PKCS12_bio(bio, certKeyPair->p12);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
|
||||
BIO_free(bio);
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (NSData*)getCertFromCertKeyPair:(CertKeyPair*)certKeyPair {
|
||||
BIO* bio = BIO_new(BIO_s_mem());
|
||||
|
||||
PEM_write_bio_X509(bio, certKeyPair->x509);
|
||||
|
||||
BUF_MEM* mem;
|
||||
BIO_get_mem_ptr(bio, &mem);
|
||||
NSData* data = [NSData dataWithBytes:mem->data length:mem->length];
|
||||
BIO_free(bio);
|
||||
return data;
|
||||
}
|
||||
|
||||
+ (void) generateKeyPairUsingSSL {
|
||||
static dispatch_once_t pred;
|
||||
dispatch_once(&pred, ^{
|
||||
if (![CryptoManager keyPairExists]) {
|
||||
|
||||
Log(LOG_I, @"Generating Certificate... ");
|
||||
CertKeyPair certKeyPair = generateCertKeyPair();
|
||||
|
||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
||||
NSString* documentsDirectory = [paths objectAtIndex:0];
|
||||
NSString* certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
|
||||
NSString* keyPairFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
||||
NSString* p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
|
||||
NSData* certData = [CryptoManager getCertFromCertKeyPair:&certKeyPair];
|
||||
NSData* p12Data = [CryptoManager getP12FromCertKeyPair:&certKeyPair];
|
||||
NSData* keyData = [CryptoManager getKeyFromCertKeyPair:&certKeyPair];
|
||||
|
||||
//Log(LOG_D, @"Writing cert and key to: \n%@\n%@", certFile, keyPairFile);
|
||||
saveCertKeyPair([certFile UTF8String], [p12File UTF8String], [keyPairFile UTF8String], certKeyPair);
|
||||
freeCertKeyPair(certKeyPair);
|
||||
|
||||
[CryptoManager writeCryptoObject:@"client.crt" data:certData];
|
||||
[CryptoManager writeCryptoObject:@"client.p12" data:p12Data];
|
||||
[CryptoManager writeCryptoObject:@"client.key" data:keyData];
|
||||
|
||||
Log(LOG_I, @"Certificate created");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface IdManager : NSObject
|
||||
|
||||
+ (NSString*) getUniqueId;
|
||||
|
||||
@@ -19,7 +19,7 @@ static const int NUM_YEARS = 10;
|
||||
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
|
||||
int add_ext(X509 *cert, int nid, char *value);
|
||||
|
||||
struct CertKeyPair generateCertKeyPair() {
|
||||
struct CertKeyPair generateCertKeyPair(void) {
|
||||
BIO *bio_err;
|
||||
X509 *x509 = NULL;
|
||||
EVP_PKEY *pkey = NULL;
|
||||
@@ -62,21 +62,6 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
|
||||
PKCS12_free(certKeyPair.p12);
|
||||
}
|
||||
|
||||
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair) {
|
||||
FILE* certFilePtr = fopen(certFile, "w");
|
||||
FILE* keyPairFilePtr = fopen(keyPairFile, "w");
|
||||
FILE* p12FilePtr = fopen(p12File, "wb");
|
||||
|
||||
//TODO: error check
|
||||
PEM_write_PrivateKey(keyPairFilePtr, certKeyPair.pkey, NULL, NULL, 0, NULL, NULL);
|
||||
PEM_write_X509(certFilePtr, certKeyPair.x509);
|
||||
i2d_PKCS12_fp(p12FilePtr, certKeyPair.p12);
|
||||
|
||||
fclose(p12FilePtr);
|
||||
fclose(certFilePtr);
|
||||
fclose(keyPairFilePtr);
|
||||
}
|
||||
|
||||
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
X509 *x;
|
||||
EVP_PKEY *pk;
|
||||
@@ -122,13 +107,7 @@ int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
*/
|
||||
X509_set_issuer_name(x, name);
|
||||
|
||||
/* Add various extensions: standard extensions */
|
||||
add_ext(x, NID_basic_constraints, "critical,CA:TRUE");
|
||||
add_ext(x, NID_key_usage, "critical,keyCertSign,cRLSign");
|
||||
|
||||
add_ext(x, NID_subject_key_identifier, "hash");
|
||||
|
||||
if (!X509_sign(x, pk, EVP_sha1())) {
|
||||
if (!X509_sign(x, pk, EVP_sha256())) {
|
||||
goto err;
|
||||
}
|
||||
|
||||
@@ -139,29 +118,3 @@ int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||
err:
|
||||
return(0);
|
||||
}
|
||||
|
||||
/* Add extension using V3 code: we can set the config file as NULL
|
||||
* because we wont reference any other sections.
|
||||
*/
|
||||
|
||||
int add_ext(X509 *cert, int nid, char *value)
|
||||
{
|
||||
X509_EXTENSION *ex;
|
||||
X509V3_CTX ctx;
|
||||
/* This sets the 'context' of the extensions. */
|
||||
/* No configuration database */
|
||||
X509V3_set_ctx_nodb(&ctx);
|
||||
/* Issuer and subject certs: both the target since it is self signed,
|
||||
* no request and no CRL
|
||||
*/
|
||||
X509V3_set_ctx(&ctx, cert, cert, NULL, NULL, 0);
|
||||
ex = X509V3_EXT_conf_nid(NULL, &ctx, nid, value);
|
||||
if (!ex) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
X509_add_ext(cert, ex, -1);
|
||||
X509_EXTENSION_free(ex);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,7 @@ typedef struct CertKeyPair {
|
||||
PKCS12 *p12;
|
||||
} CertKeyPair;
|
||||
|
||||
struct CertKeyPair generateCertKeyPair();
|
||||
struct CertKeyPair generateCertKeyPair(void);
|
||||
void freeCertKeyPair(CertKeyPair);
|
||||
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair);
|
||||
#endif
|
||||
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
//
|
||||
// App+CoreDataProperties.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
|
||||
// to keep your implementation up to date with your model.
|
||||
//
|
||||
|
||||
#import "App.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface App (CoreDataProperties)
|
||||
|
||||
@property (nullable, nonatomic, retain) NSString *id;
|
||||
@property (nullable, nonatomic, retain) NSData *image;
|
||||
@property (nullable, nonatomic, retain) NSString *name;
|
||||
@property (nullable, nonatomic, retain) Host *host;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,18 +0,0 @@
|
||||
//
|
||||
// App.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
@class Host;
|
||||
|
||||
@interface App : NSManagedObject
|
||||
|
||||
@end
|
||||
|
||||
#import "App+CoreDataProperties.h"
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// App.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "App.h"
|
||||
|
||||
@implementation App
|
||||
|
||||
@end
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "AppDelegate.h"
|
||||
#import "TemporaryHost.h"
|
||||
#import "TemporaryApp.h"
|
||||
@@ -14,12 +13,21 @@
|
||||
|
||||
@interface DataManager : NSObject
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls;
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
remote:(BOOL)streamingRemotely
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr;
|
||||
|
||||
- (NSArray*) getHosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
||||
- (void) updateIconForExistingApp:(TemporaryApp*)app;
|
||||
- (void) removeHost:(TemporaryHost*)host;
|
||||
- (void) removeApp:(TemporaryApp*)app;
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
#import "DataManager.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "TemporarySettings.h"
|
||||
#import "Settings.h"
|
||||
|
||||
@implementation DataManager {
|
||||
NSManagedObjectContext *_managedObjectContext;
|
||||
@@ -22,16 +21,17 @@
|
||||
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
|
||||
// Main Thread Checker happy.
|
||||
if ([NSThread isMainThread]) {
|
||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
||||
_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||
|
||||
}
|
||||
else {
|
||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
||||
self->_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||
});
|
||||
}
|
||||
|
||||
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||
[_managedObjectContext setPersistentStoreCoordinator:_appDelegate.persistentStoreCoordinator];
|
||||
[_managedObjectContext setParentContext:[_appDelegate managedObjectContext]];
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -53,16 +53,31 @@
|
||||
return uid;
|
||||
}
|
||||
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate framerate:(NSInteger)framerate height:(NSInteger)height width:(NSInteger)width onscreenControls:(NSInteger)onscreenControls {
|
||||
- (void) saveSettingsWithBitrate:(NSInteger)bitrate
|
||||
framerate:(NSInteger)framerate
|
||||
height:(NSInteger)height
|
||||
width:(NSInteger)width
|
||||
onscreenControls:(NSInteger)onscreenControls
|
||||
remote:(BOOL)streamingRemotely
|
||||
optimizeGames:(BOOL)optimizeGames
|
||||
multiController:(BOOL)multiController
|
||||
audioOnPC:(BOOL)audioOnPC
|
||||
useHevc:(BOOL)useHevc
|
||||
enableHdr:(BOOL)enableHdr {
|
||||
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Settings* settingsToSave = [self retrieveSettings];
|
||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||
// Bitrate is persisted in kbps
|
||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
||||
settingsToSave.streamingRemotely = streamingRemotely;
|
||||
settingsToSave.optimizeGames = optimizeGames;
|
||||
settingsToSave.multiController = multiController;
|
||||
settingsToSave.playAudioOnPC = audioOnPC;
|
||||
settingsToSave.useHevc = useHevc;
|
||||
settingsToSave.enableHdr = enableHdr;
|
||||
|
||||
[self saveData];
|
||||
}];
|
||||
@@ -73,8 +88,8 @@
|
||||
// Add a new persistent managed object if one doesn't exist
|
||||
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
||||
if (parent == nil) {
|
||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:_managedObjectContext];
|
||||
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
|
||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:self->_managedObjectContext];
|
||||
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
||||
}
|
||||
|
||||
// Push changes from the temp host to the persistent one
|
||||
@@ -98,8 +113,8 @@
|
||||
// Add a new persistent managed object if one doesn't exist
|
||||
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
|
||||
if (parentApp == nil) {
|
||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:_managedObjectContext];
|
||||
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
|
||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:self->_managedObjectContext];
|
||||
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
||||
}
|
||||
|
||||
[app propagateChangesToParent:parentApp withHost:parent];
|
||||
@@ -113,20 +128,6 @@
|
||||
}];
|
||||
}
|
||||
|
||||
- (void) updateIconForExistingApp:(TemporaryApp*)app {
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
|
||||
if (parentApp == nil) {
|
||||
// The app must exist to be updated
|
||||
return;
|
||||
}
|
||||
|
||||
parentApp.image = app.image;
|
||||
|
||||
[self saveData];
|
||||
}];
|
||||
}
|
||||
|
||||
- (TemporarySettings*) getSettings {
|
||||
__block TemporarySettings *tempSettings;
|
||||
|
||||
@@ -155,7 +156,7 @@
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
|
||||
if (managedApp != nil) {
|
||||
[_managedObjectContext deleteObject:managedApp];
|
||||
[self->_managedObjectContext deleteObject:managedApp];
|
||||
[self saveData];
|
||||
}
|
||||
}];
|
||||
@@ -165,7 +166,7 @@
|
||||
[_managedObjectContext performBlockAndWait:^{
|
||||
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
||||
if (managedHost != nil) {
|
||||
[_managedObjectContext deleteObject:managedHost];
|
||||
[self->_managedObjectContext deleteObject:managedHost];
|
||||
[self saveData];
|
||||
}
|
||||
}];
|
||||
@@ -173,7 +174,7 @@
|
||||
|
||||
- (void) saveData {
|
||||
NSError* error;
|
||||
if (![_managedObjectContext save:&error]) {
|
||||
if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
|
||||
Log(LOG_E, @"Unable to save hosts to database: %@", error);
|
||||
}
|
||||
|
||||
@@ -209,7 +210,7 @@
|
||||
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
|
||||
for (App* app in apps) {
|
||||
if ([app.id isEqualToString:tempApp.id] &&
|
||||
[app.host.uuid isEqualToString:app.host.uuid]) {
|
||||
[app.host.uuid isEqualToString:tempApp.host.uuid]) {
|
||||
return app;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
//
|
||||
// Host+CoreDataProperties.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
// Delete this file and regenerate it using "Create NSManagedObject Subclass…"
|
||||
// to keep your implementation up to date with your model.
|
||||
//
|
||||
|
||||
#import "Host.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface Host (CoreDataProperties)
|
||||
|
||||
@property (nullable, nonatomic, retain) NSString *address;
|
||||
@property (nullable, nonatomic, retain) NSString *externalAddress;
|
||||
@property (nullable, nonatomic, retain) NSString *localAddress;
|
||||
@property (nullable, nonatomic, retain) NSString *mac;
|
||||
@property (nullable, nonatomic, retain) NSString *name;
|
||||
@property (nullable, nonatomic, retain) NSNumber *pairState;
|
||||
@property (nullable, nonatomic, retain) NSString *uuid;
|
||||
@property (nullable, nonatomic, retain) NSSet *appList;
|
||||
|
||||
@end
|
||||
|
||||
@interface Host (CoreDataGeneratedAccessors)
|
||||
|
||||
- (void)addAppListObject:(NSManagedObject *)value;
|
||||
- (void)removeAppListObject:(NSManagedObject *)value;
|
||||
- (void)addAppList:(NSSet *)values;
|
||||
- (void)removeAppList:(NSSet *)values;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
@@ -1,17 +0,0 @@
|
||||
//
|
||||
// Host.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "Utils.h"
|
||||
|
||||
@interface Host : NSManagedObject
|
||||
|
||||
@end
|
||||
|
||||
#import "Host+CoreDataProperties.h"
|
||||
@@ -1,13 +0,0 @@
|
||||
//
|
||||
// Host.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 7/10/15.
|
||||
// Copyright © 2015 Limelight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Host.h"
|
||||
|
||||
@implementation Host
|
||||
|
||||
@end
|
||||
@@ -1,22 +0,0 @@
|
||||
//
|
||||
// Settings.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
|
||||
|
||||
@interface Settings : NSManagedObject
|
||||
|
||||
@property (nonatomic, retain) NSNumber * bitrate;
|
||||
@property (nonatomic, retain) NSNumber * framerate;
|
||||
@property (nonatomic, retain) NSNumber * height;
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
|
||||
@end
|
||||
@@ -1,21 +0,0 @@
|
||||
//
|
||||
// Settings.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 10/28/14.
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import "Settings.h"
|
||||
|
||||
|
||||
@implementation Settings
|
||||
|
||||
@dynamic bitrate;
|
||||
@dynamic framerate;
|
||||
@dynamic height;
|
||||
@dynamic width;
|
||||
@dynamic onscreenControls;
|
||||
@dynamic uniqueId;
|
||||
|
||||
@end
|
||||
@@ -6,15 +6,15 @@
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TemporaryHost.h"
|
||||
#import "App.h"
|
||||
#import "App+CoreDataClass.h"
|
||||
|
||||
@interface TemporaryApp : NSObject
|
||||
|
||||
@property (nullable, nonatomic, retain) NSString *id;
|
||||
@property (nullable, nonatomic, retain) NSData *image;
|
||||
@property (nullable, nonatomic, retain) NSString *name;
|
||||
@property (nullable, nonatomic, retain) NSString *installPath;
|
||||
@property (nonatomic) BOOL hdrSupported;
|
||||
@property (nullable, nonatomic, retain) TemporaryHost *host;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
//
|
||||
|
||||
#import "TemporaryApp.h"
|
||||
#import "App.h"
|
||||
|
||||
@implementation TemporaryApp
|
||||
|
||||
@@ -15,8 +14,8 @@
|
||||
self = [self init];
|
||||
|
||||
self.id = app.id;
|
||||
self.image = app.image;
|
||||
self.name = app.name;
|
||||
self.hdrSupported = app.hdrSupported;
|
||||
self.host = tempHost;
|
||||
|
||||
return self;
|
||||
@@ -25,6 +24,7 @@
|
||||
- (void) propagateChangesToParent:(App*)parent withHost:(Host*)host {
|
||||
parent.id = self.id;
|
||||
parent.name = self.name;
|
||||
parent.hdrSupported = self.hdrSupported;
|
||||
parent.host = host;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,23 +6,26 @@
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Utils.h"
|
||||
#import "Host.h"
|
||||
#import "Host+CoreDataClass.h"
|
||||
|
||||
@interface TemporaryHost : NSObject
|
||||
|
||||
@property (nonatomic) BOOL online;
|
||||
@property (nonatomic) State state;
|
||||
@property (nonatomic) PairState pairState;
|
||||
@property (nonatomic, nullable) NSString * activeAddress;
|
||||
@property (nonatomic, nullable) NSString * currentGame;
|
||||
|
||||
@property (nonatomic, nullable, retain) NSData *serverCert;
|
||||
@property (nonatomic, nullable, retain) NSString *address;
|
||||
@property (nonatomic, nullable, retain) NSString *externalAddress;
|
||||
@property (nonatomic, nullable, retain) NSString *localAddress;
|
||||
@property (nonatomic, nullable, retain) NSString *ipv6Address;
|
||||
@property (nonatomic, nullable, retain) NSString *mac;
|
||||
@property (nonatomic) int serverCodecModeSupport;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@property (nonatomic, retain) NSString *address;
|
||||
@property (nonatomic, retain) NSString *externalAddress;
|
||||
@property (nonatomic, retain) NSString *localAddress;
|
||||
@property (nonatomic, retain) NSString *mac;
|
||||
@property (nonatomic, retain) NSString *name;
|
||||
@property (nonatomic, retain) NSString *uuid;
|
||||
@property (nonatomic, retain) NSMutableSet *appList;
|
||||
|
||||
@@ -8,9 +8,7 @@
|
||||
|
||||
#import "DataManager.h"
|
||||
#import "TemporaryHost.h"
|
||||
#import "Host.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "App.h"
|
||||
|
||||
@implementation TemporaryHost
|
||||
|
||||
@@ -18,6 +16,7 @@
|
||||
self = [super init];
|
||||
self.appList = [[NSMutableSet alloc] init];
|
||||
self.currentGame = @"0";
|
||||
self.state = StateUnknown;
|
||||
|
||||
return self;
|
||||
}
|
||||
@@ -28,10 +27,15 @@
|
||||
self.address = host.address;
|
||||
self.externalAddress = host.externalAddress;
|
||||
self.localAddress = host.localAddress;
|
||||
self.ipv6Address = host.ipv6Address;
|
||||
self.mac = host.mac;
|
||||
self.name = host.name;
|
||||
self.uuid = host.uuid;
|
||||
self.pairState = [host.pairState intValue];
|
||||
self.serverCodecModeSupport = host.serverCodecModeSupport;
|
||||
self.serverCert = host.serverCert;
|
||||
|
||||
// Ensure we don't use a stale cached pair state if we haven't pinned the cert yet
|
||||
self.pairState = host.serverCert ? [host.pairState intValue] : PairStateUnpaired;
|
||||
|
||||
NSMutableSet *appList = [[NSMutableSet alloc] init];
|
||||
|
||||
@@ -46,12 +50,30 @@
|
||||
}
|
||||
|
||||
- (void) propagateChangesToParent:(Host*)parentHost {
|
||||
parentHost.address = self.address;
|
||||
parentHost.externalAddress = self.externalAddress;
|
||||
parentHost.localAddress = self.localAddress;
|
||||
parentHost.mac = self.mac;
|
||||
// Avoid overwriting existing data with nil if
|
||||
// we don't have everything populated in the temporary
|
||||
// host.
|
||||
if (self.address != nil) {
|
||||
parentHost.address = self.address;
|
||||
}
|
||||
if (self.externalAddress != nil) {
|
||||
parentHost.externalAddress = self.externalAddress;
|
||||
}
|
||||
if (self.localAddress != nil) {
|
||||
parentHost.localAddress = self.localAddress;
|
||||
}
|
||||
if (self.ipv6Address != nil) {
|
||||
parentHost.ipv6Address = self.ipv6Address;
|
||||
}
|
||||
if (self.mac != nil) {
|
||||
parentHost.mac = self.mac;
|
||||
}
|
||||
if (self.serverCert != nil) {
|
||||
parentHost.serverCert = self.serverCert;
|
||||
}
|
||||
parentHost.name = self.name;
|
||||
parentHost.uuid = self.uuid;
|
||||
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
|
||||
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
|
||||
}
|
||||
|
||||
|
||||
@@ -6,8 +6,7 @@
|
||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "Settings.h"
|
||||
#import "Settings+CoreDataClass.h"
|
||||
|
||||
@interface TemporarySettings : NSObject
|
||||
|
||||
@@ -19,6 +18,12 @@
|
||||
@property (nonatomic, retain) NSNumber * width;
|
||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||
@property (nonatomic, retain) NSString * uniqueId;
|
||||
@property (nonatomic) BOOL streamingRemotely;
|
||||
@property (nonatomic) BOOL useHevc;
|
||||
@property (nonatomic) BOOL multiController;
|
||||
@property (nonatomic) BOOL playAudioOnPC;
|
||||
@property (nonatomic) BOOL optimizeGames;
|
||||
@property (nonatomic) BOOL enableHdr;
|
||||
|
||||
- (id) initFromSettings:(Settings*)settings;
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
#import "TemporarySettings.h"
|
||||
#import "OnScreenControls.h"
|
||||
|
||||
@implementation TemporarySettings
|
||||
|
||||
@@ -15,12 +16,59 @@
|
||||
|
||||
self.parent = settings;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
NSInteger _bitrate = [[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"];
|
||||
NSInteger _framerate = [[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"];
|
||||
|
||||
if (_bitrate) {
|
||||
self.bitrate = [NSNumber numberWithInteger:_bitrate];
|
||||
} else {
|
||||
self.bitrate = [NSNumber numberWithInteger:20000];
|
||||
}
|
||||
|
||||
if (_framerate) {
|
||||
self.framerate = [NSNumber numberWithInteger:_framerate];
|
||||
} else {
|
||||
self.framerate = [NSNumber numberWithInteger:60];
|
||||
}
|
||||
|
||||
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"] || NO;
|
||||
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"] || NO;
|
||||
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"] || NO;
|
||||
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"] || YES;
|
||||
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"] || YES;
|
||||
|
||||
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||
switch (_screenSize) {
|
||||
case 0:
|
||||
self.height = [NSNumber numberWithInteger:720];
|
||||
self.width = [NSNumber numberWithInteger:1280];
|
||||
break;
|
||||
case 2:
|
||||
self.height = [NSNumber numberWithInteger:2160];
|
||||
self.width = [NSNumber numberWithInteger:3840];
|
||||
break;
|
||||
case 1:
|
||||
default:
|
||||
self.height = [NSNumber numberWithInteger:1080];
|
||||
self.width = [NSNumber numberWithInteger:1920];
|
||||
break;
|
||||
}
|
||||
self.onscreenControls = [NSNumber numberWithInteger:OnScreenControlsLevelOff];
|
||||
#else
|
||||
self.bitrate = settings.bitrate;
|
||||
self.framerate = settings.framerate;
|
||||
self.height = settings.height;
|
||||
self.width = settings.width;
|
||||
self.useHevc = settings.useHevc;
|
||||
self.playAudioOnPC = settings.playAudioOnPC;
|
||||
self.enableHdr = settings.enableHdr;
|
||||
self.optimizeGames = settings.optimizeGames;
|
||||
self.multiController = settings.multiController;
|
||||
self.onscreenControls = settings.onscreenControls;
|
||||
#endif
|
||||
self.uniqueId = settings.uniqueId;
|
||||
self.streamingRemotely = settings.streamingRemotely;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -1,203 +0,0 @@
|
||||
Font data copyright Google 2012
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "limelight_computer_add_icon_1x.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "limelight_computer_add_icon_2x.png"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "limelight_computer_add_icon_3x.png"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB |
|
Before Width: | Height: | Size: 2.4 KiB |
@@ -2,11 +2,11 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "add.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "logo.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
@@ -0,0 +1,69 @@
|
||||
%PDF-1.5
|
||||
%µí®û
|
||||
4 0 obj
|
||||
<< /Length 5 0 R
|
||||
/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
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
212
|
||||
endobj
|
||||
3 0 obj
|
||||
<<
|
||||
/ExtGState <<
|
||||
/a0 << /CA 0.3 /ca 0.3 >>
|
||||
/a1 << /CA 1 /ca 1 >>
|
||||
>>
|
||||
>>
|
||||
endobj
|
||||
2 0 obj
|
||||
<< /Type /Page % 1
|
||||
/Parent 1 0 R
|
||||
/MediaBox [ 0 0 225 225 ]
|
||||
/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:20190830180928-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000648 00000 n
|
||||
0000000430 00000 n
|
||||
0000000326 00000 n
|
||||
0000000015 00000 n
|
||||
0000000304 00000 n
|
||||
0000000713 00000 n
|
||||
0000000829 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
881
|
||||
%%EOF
|
||||
@@ -2,18 +2,16 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "limelight_computer_1x.png"
|
||||
"filename" : "Computer.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "limelight_computer_2x.png"
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "limelight_computer_3x.png"
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 13 KiB |
|
Before Width: | Height: | Size: 22 KiB |
@@ -2,17 +2,15 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "pull-arrow-light-14x31@1x.png",
|
||||
"filename" : "error.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "pull-arrow-light-28x61@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "pull-arrow-light-42x92@3x.png",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
@@ -0,0 +1,68 @@
|
||||
%PDF-1.5
|
||||
%µí®û
|
||||
4 0 obj
|
||||
<< /Length 5 0 R
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœeŒÑ Ã0Dÿ5Å-Uªì(kt„Rhò¤Ýª8µ �t︓Bb�å¯B)öù̸=ó—Œõž1fã¸òä�W˜sW½P’jçjXbó†kG Ý’rî%c?õì=ÓãÍ?ÜðÒ~Óƒ~®L+·
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
115
|
||||
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:20190830180932-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000517 00000 n
|
||||
0000000301 00000 n
|
||||
0000000229 00000 n
|
||||
0000000015 00000 n
|
||||
0000000207 00000 n
|
||||
0000000582 00000 n
|
||||
0000000698 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
750
|
||||
%%EOF
|
||||
@@ -2,18 +2,17 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "L2_60x60@1x.png"
|
||||
"filename" : "L2_60x60@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "L2_120x120@2x.png"
|
||||
"filename" : "L2_120x120@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "L2_180x180@3x.png"
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "lock.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
%PDF-1.5
|
||||
%µí®û
|
||||
4 0 obj
|
||||
<< /Length 5 0 R
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœUQKnÅ Üû¾@]ÌÇðŽñŽP!õu‘,ÚÞ_*ø¥"‚ÐÌØcZë�×Öξ�q¯Ÿ¾$|ýBÊ␍×wb+Ć�À<¨ßiÒ£ŠvW:9œ˜�O™•¸*˜ZÎ šfwΰ çåœ;␍ΆïZ§ÙÛá¸Ñ£×¢¨›y‚xœõ)Ñσd§r³�ËzÚa™Ç2ºUãòõp�oâè(|T'–JIrpóiI+4²Æ8´6§…êªÀd€5‘ÄxÏ␍v•Η¢_:'áiÔò tíöËïºk'¶vÎèP{ŽÙûЂ®¼$X2q|h“ê†Wv¦Q«?¹¹«‚ù_ùŸý Oøp…&
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
267
|
||||
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:20190830180930-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000669 00000 n
|
||||
0000000453 00000 n
|
||||
0000000381 00000 n
|
||||
0000000015 00000 n
|
||||
0000000359 00000 n
|
||||
0000000734 00000 n
|
||||
0000000850 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
902
|
||||
%%EOF
|
||||
|
Before Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 901 B |
|
Before Width: | Height: | Size: 812 B |
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -2,18 +2,17 @@
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x",
|
||||
"filename" : "R2_60x60@1x.png"
|
||||
"filename" : "R2_60x60@1x.png",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x",
|
||||
"filename" : "R2_120x120@2x.png"
|
||||
"filename" : "R2_120x120@2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x",
|
||||
"filename" : "R2_120x120@3x.png"
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
|
||||
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "settings.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
%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,21 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"filename" : "up_arrow.pdf",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"version" : 1,
|
||||
"author" : "xcode"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
%PDF-1.5
|
||||
%µí®û
|
||||
4 0 obj
|
||||
<< /Length 5 0 R
|
||||
/Filter /FlateDecode
|
||||
>>
|
||||
stream
|
||||
xœUŽQ
|
||||
ƒ@Dÿsй€1ÙÍF=†G(…êÇú¡½?-e`ƒÌ$
|
||||
5Ö¼ZIqh›Ð¾Ó—4qAî¨ó ÓsTdã¹dl>üØ�H… {º‰n|–KÏâé,Ë}°?‡ê5ôª™ÿ^øÐH;U$}
|
||||
endstream
|
||||
endobj
|
||||
5 0 obj
|
||||
114
|
||||
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:20190830203401-07'00)
|
||||
>>
|
||||
endobj
|
||||
7 0 obj
|
||||
<< /Type /Catalog
|
||||
/Pages 1 0 R
|
||||
>>
|
||||
endobj
|
||||
xref
|
||||
0 8
|
||||
0000000000 65535 f
|
||||
0000000516 00000 n
|
||||
0000000300 00000 n
|
||||
0000000228 00000 n
|
||||
0000000015 00000 n
|
||||
0000000206 00000 n
|
||||
0000000581 00000 n
|
||||
0000000697 00000 n
|
||||
trailer
|
||||
<< /Size 8
|
||||
/Root 7 0 R
|
||||
/Info 6 0 R
|
||||
>>
|
||||
startxref
|
||||
749
|
||||
%%EOF
|
||||
@@ -0,0 +1,26 @@
|
||||
//
|
||||
// Controller.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 2/11/19.
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
@import GameController;
|
||||
|
||||
@interface Controller : NSObject
|
||||
|
||||
@property (nullable, nonatomic, retain) GCController* gamepad;
|
||||
@property (nonatomic) int playerIndex;
|
||||
@property (nonatomic) int lastButtonFlags;
|
||||
@property (nonatomic) int emulatingButtonFlags;
|
||||
@property (nonatomic) unsigned char lastLeftTrigger;
|
||||
@property (nonatomic) unsigned char lastRightTrigger;
|
||||
@property (nonatomic) short lastLeftStickX;
|
||||
@property (nonatomic) short lastLeftStickY;
|
||||
@property (nonatomic) short lastRightStickX;
|
||||
@property (nonatomic) short lastRightStickY;
|
||||
@property (nonatomic) unsigned short lowFreqMotor;
|
||||
@property (nonatomic) unsigned short highFreqMotor;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,13 @@
|
||||
//
|
||||
// Controller.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Cameron Gutman on 2/11/19.
|
||||
// Copyright © 2019 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#include "Controller.h"
|
||||
|
||||
@implementation Controller
|
||||
|
||||
@end
|
||||
@@ -1,26 +0,0 @@
|
||||
//
|
||||
// Controller.swift
|
||||
// Moonlight
|
||||
//
|
||||
// Created by David Aghassi on 4/11/16.
|
||||
// Copyright © 2016 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
@objc
|
||||
/**
|
||||
Defines a controller layout
|
||||
*/
|
||||
class Controller: NSObject {
|
||||
// Swift requires initial properties
|
||||
var playerIndex: CInt = 0 // Controller number (e.g. 1, 2 ,3 etc)
|
||||
var lastButtonFlags: CInt = 0
|
||||
var emulatingButtonFlags: CInt = 0
|
||||
var lastLeftTrigger: CChar = 0 // Last left trigger pressed
|
||||
var lastRightTrigger: CChar = 0 // Last right trigger pressed
|
||||
var lastLeftStickX: CShort = 0 // Last X direction the left joystick went
|
||||
var lastLeftStickY: CShort = 0 // Last Y direction the left joystick went
|
||||
var lastRightStickX: CShort = 0 // Last X direction the right joystick went
|
||||
var lastRightStickY: CShort = 0 // Last Y direction the right joystick went
|
||||
}
|
||||
@@ -6,19 +6,28 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
// Swift
|
||||
#import "Moonlight-Swift.h"
|
||||
@class Controller;
|
||||
#if !TARGET_OS_IPHONE
|
||||
#import "Gamepad.h"
|
||||
#endif
|
||||
#import "StreamConfiguration.h"
|
||||
#import "Controller.h"
|
||||
|
||||
@class OnScreenControls;
|
||||
|
||||
@interface ControllerSupport : NSObject
|
||||
|
||||
-(id) init;
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
||||
-(void) cleanup;
|
||||
-(Controller*) getOscController;
|
||||
#else
|
||||
-(void) assignGamepad:(struct Gamepad_device *)gamepad;
|
||||
-(void) removeGamepad:(struct Gamepad_device *)gamepad;
|
||||
-(NSMutableDictionary*) getControllers;
|
||||
#endif
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
-(void) updateRightStick:(Controller*)controller x:(short)x y:(short)y;
|
||||
@@ -32,7 +41,10 @@
|
||||
-(void) clearButtonFlag:(Controller*)controller flags:(int)flags;
|
||||
|
||||
-(void) updateFinished:(Controller*)controller;
|
||||
-(Controller*) getOscController;
|
||||
|
||||
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||
|
||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig;
|
||||
|
||||
@property (nonatomic, strong) id connectObserver;
|
||||
@property (nonatomic, strong) id disconnectObserver;
|
||||
|
||||
@@ -7,18 +7,24 @@
|
||||
//
|
||||
|
||||
#import "ControllerSupport.h"
|
||||
#import "Controller.h"
|
||||
|
||||
#import "OnScreenControls.h"
|
||||
#if !TARGET_OS_IPHONE
|
||||
#import "Gamepad.h"
|
||||
#import "Control.h"
|
||||
#endif
|
||||
|
||||
#import "DataManager.h"
|
||||
#include "Limelight.h"
|
||||
|
||||
// Swift
|
||||
#import "Moonlight-Swift.h"
|
||||
@class Controller;
|
||||
|
||||
@import GameController;
|
||||
@import AudioToolbox;
|
||||
|
||||
@implementation ControllerSupport {
|
||||
NSLock *_controllerStreamLock;
|
||||
NSMutableDictionary *_controllers;
|
||||
NSTimer *_rumbleTimer;
|
||||
|
||||
OnScreenControls *_osc;
|
||||
|
||||
@@ -26,16 +32,52 @@
|
||||
// and player 0
|
||||
Controller *_player0osc;
|
||||
|
||||
char _controllerNumbers;
|
||||
|
||||
#define EMULATING_SELECT 0x1
|
||||
#define EMULATING_SPECIAL 0x2
|
||||
|
||||
bool _oscEnabled;
|
||||
char _controllerNumbers;
|
||||
bool _multiController;
|
||||
}
|
||||
|
||||
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
|
||||
#define UPDATE_BUTTON_FLAG(controller, x, y) \
|
||||
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
|
||||
|
||||
-(void) rumbleController: (Controller*)controller
|
||||
{
|
||||
#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]];
|
||||
if (controller == nil && controllerNumber == 0 && _oscEnabled) {
|
||||
// No physical controller, but we have on-screen controls
|
||||
controller = _player0osc;
|
||||
}
|
||||
if (controller == nil) {
|
||||
// No connected controller for this player
|
||||
return;
|
||||
}
|
||||
|
||||
// Update the motor levels for the rumble timer to grab next iteration
|
||||
controller.lowFreqMotor = lowFreqMotor;
|
||||
controller.highFreqMotor = highFreqMotor;
|
||||
|
||||
// Rumble now to ensure short vibrations aren't missed
|
||||
[self rumbleController:controller];
|
||||
}
|
||||
|
||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
|
||||
{
|
||||
@synchronized(controller) {
|
||||
@@ -112,17 +154,16 @@
|
||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
|
||||
{
|
||||
@synchronized(controller) {
|
||||
int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
|
||||
int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
|
||||
|
||||
controller.lastButtonFlags = flags;
|
||||
|
||||
// This must be called before handleSpecialCombosPressed
|
||||
// because we clear the original button flags there
|
||||
int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
|
||||
int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
|
||||
|
||||
[self handleSpecialCombosReleased:controller releasedButtons:releasedButtons];
|
||||
|
||||
[self handleSpecialCombosPressed:controller pressedButtons:pressedButtons];
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -147,11 +188,15 @@
|
||||
[_controllerStreamLock lock];
|
||||
@synchronized(controller) {
|
||||
// Player 1 is always present for OSC
|
||||
LiSendMultiControllerEvent(controller.playerIndex, _controllerNumbers | 1, controller.lastButtonFlags, controller.lastLeftTrigger, controller.lastRightTrigger, controller.lastLeftStickX, controller.lastLeftStickY, controller.lastRightStickX, controller.lastRightStickY);
|
||||
LiSendMultiControllerEvent(_multiController ? controller.playerIndex : 0,
|
||||
(_multiController ? _controllerNumbers : 1) | (_oscEnabled ? 1 : 0), controller.lastButtonFlags, controller.lastLeftTrigger, controller.lastRightTrigger, controller.lastLeftStickX, controller.lastLeftStickY, controller.lastRightStickX, controller.lastRightStickY);
|
||||
}
|
||||
[_controllerStreamLock unlock];
|
||||
}
|
||||
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
|
||||
-(void) unregisterControllerCallbacks:(GCController*) controller
|
||||
{
|
||||
if (controller != NULL) {
|
||||
@@ -169,21 +214,32 @@
|
||||
-(void) registerControllerCallbacks:(GCController*) controller
|
||||
{
|
||||
if (controller != NULL) {
|
||||
controller.controllerPausedHandler = ^(GCController *controller) {
|
||||
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
[self setButtonFlag:limeController flags:PLAY_FLAG];
|
||||
[self updateFinished:limeController];
|
||||
|
||||
// Pause for 100 ms
|
||||
usleep(100 * 1000);
|
||||
|
||||
[self clearButtonFlag:limeController flags:PLAY_FLAG];
|
||||
[self updateFinished:limeController];
|
||||
};
|
||||
// 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.
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
// Nothing - buttonMenu is already handled in valueChangedHandler
|
||||
}
|
||||
else {
|
||||
controller.controllerPausedHandler = ^(GCController *controller) {
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
// Get off the main thread
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
[self setButtonFlag:limeController flags:PLAY_FLAG];
|
||||
[self updateFinished:limeController];
|
||||
|
||||
// Pause for 100 ms
|
||||
usleep(100 * 1000);
|
||||
|
||||
[self clearButtonFlag:limeController flags:PLAY_FLAG];
|
||||
[self updateFinished:limeController];
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
if (controller.extendedGamepad != NULL) {
|
||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
||||
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
short leftStickX, leftStickY;
|
||||
short rightStickX, rightStickY;
|
||||
char leftTrigger, rightTrigger;
|
||||
@@ -201,6 +257,26 @@
|
||||
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
|
||||
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
|
||||
|
||||
// Yay, iOS 12.1 now supports analog stick buttons
|
||||
if (@available(iOS 12.1, tvOS 12.1, *)) {
|
||||
if (gamepad.leftThumbstickButton != nil) {
|
||||
UPDATE_BUTTON_FLAG(limeController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed);
|
||||
}
|
||||
if (gamepad.rightThumbstickButton != nil) {
|
||||
UPDATE_BUTTON_FLAG(limeController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
||||
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
||||
|
||||
@@ -218,7 +294,7 @@
|
||||
}
|
||||
else if (controller.gamepad != NULL) {
|
||||
controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
|
||||
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:gamepad.controller.playerIndex]];
|
||||
UPDATE_BUTTON_FLAG(limeController, A_FLAG, gamepad.buttonA.pressed);
|
||||
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
|
||||
UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
|
||||
@@ -257,6 +333,18 @@
|
||||
if (controller != NULL) {
|
||||
if (controller.extendedGamepad != NULL) {
|
||||
level = OnScreenControlsLevelAutoGCExtendedGamepad;
|
||||
if (@available(iOS 12.1, tvOS 12.1, *)) {
|
||||
if (controller.extendedGamepad.leftThumbstickButton != nil &&
|
||||
controller.extendedGamepad.rightThumbstickButton != nil) {
|
||||
level = OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons;
|
||||
if (@available(iOS 13.0, tvOS 13.0, *)) {
|
||||
if (controller.extendedGamepad.buttonOptions != nil) {
|
||||
// Has L3/R3 and Select, so we can show nothing :)
|
||||
level = OnScreenControlsLevelOff;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
else if (controller.gamepad != NULL) {
|
||||
@@ -266,7 +354,9 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
[_osc setLevel:level];
|
||||
#endif
|
||||
}
|
||||
|
||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
|
||||
@@ -283,6 +373,7 @@
|
||||
controller.playerIndex = i;
|
||||
|
||||
Controller* limeController;
|
||||
|
||||
if (i == 0) {
|
||||
// Player 0 shares a controller object with the on-screen controls
|
||||
limeController = _player0osc;
|
||||
@@ -290,6 +381,8 @@
|
||||
limeController = [[Controller alloc] init];
|
||||
limeController.playerIndex = i;
|
||||
}
|
||||
|
||||
limeController.gamepad = controller;
|
||||
|
||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
@@ -299,31 +392,155 @@
|
||||
}
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
-(Controller*) getOscController {
|
||||
return _player0osc;
|
||||
}
|
||||
#else
|
||||
-(NSMutableDictionary*) getControllers {
|
||||
return _controllers;
|
||||
}
|
||||
|
||||
-(id) init
|
||||
-(void) assignGamepad:(struct Gamepad_device *)gamepad {
|
||||
for (int i = 0; i < 4; i++) {
|
||||
if (!(_controllerNumbers & (1 << i))) {
|
||||
_controllerNumbers |= (1 << i);
|
||||
gamepad->deviceID = i;
|
||||
NSLog(@"Gamepad device id: %u assigned", gamepad->deviceID);
|
||||
Controller* limeController;
|
||||
limeController = [[Controller alloc] init];
|
||||
limeController.playerIndex = i;
|
||||
|
||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:i]];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
-(void) removeGamepad:(struct Gamepad_device *)gamepad {
|
||||
_controllerNumbers &= ~(1 << gamepad->deviceID);
|
||||
Log(LOG_I, @"Unassigning controller index: %ld", (long)gamepad->deviceID);
|
||||
|
||||
// Inform the server of the updated active gamepads before removing this controller
|
||||
[self updateFinished:[_controllers objectForKey:[NSNumber numberWithInteger:gamepad->deviceID]]];
|
||||
[_controllers removeObjectForKey:[NSNumber numberWithInteger:gamepad->deviceID]];
|
||||
}
|
||||
#endif
|
||||
|
||||
+(bool) isSupportedGamepad:(GCController*) controller {
|
||||
return controller.extendedGamepad != nil || controller.gamepad != nil;
|
||||
}
|
||||
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
+(int) getGamepadCount {
|
||||
int count = 0;
|
||||
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig {
|
||||
int mask = 0;
|
||||
|
||||
if (streamConfig.multiController) {
|
||||
int i = 0;
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
mask |= 1 << i++;
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Some games don't deal with having controller reconnected
|
||||
// properly so always report controller 1 if not in MC mode
|
||||
mask = 0x1;
|
||||
}
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
|
||||
|
||||
// Even if no gamepads are present, we will always count one
|
||||
// if OSC is enabled.
|
||||
if (level != OnScreenControlsLevelOff) {
|
||||
mask |= 0x1;
|
||||
}
|
||||
#endif
|
||||
|
||||
return mask;
|
||||
}
|
||||
|
||||
-(void) rumbleTimer
|
||||
{
|
||||
for (int i = 0; i < 4; i++) {
|
||||
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
|
||||
if (controller == nil && i == 0 && _oscEnabled) {
|
||||
// No physical controller, but we have on-screen controls
|
||||
controller = _player0osc;
|
||||
}
|
||||
if (controller == nil) {
|
||||
// No connected controller for this player
|
||||
continue;
|
||||
}
|
||||
|
||||
[self rumbleController:controller];
|
||||
}
|
||||
}
|
||||
|
||||
-(id) initWithConfig:(StreamConfiguration*)streamConfig
|
||||
{
|
||||
self = [super init];
|
||||
|
||||
_controllerStreamLock = [[NSLock alloc] init];
|
||||
_controllers = [[NSMutableDictionary alloc] init];
|
||||
_controllerNumbers = 0;
|
||||
_multiController = streamConfig.multiController;
|
||||
|
||||
_player0osc = [[Controller alloc] init];
|
||||
_player0osc.playerIndex = 0;
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
DataManager* dataMan = [[DataManager alloc] init];
|
||||
_oscEnabled = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue] != OnScreenControlsLevelOff;
|
||||
#else
|
||||
_oscEnabled = false;
|
||||
initGamepad(self);
|
||||
Gamepad_detectDevices();
|
||||
#endif
|
||||
|
||||
_rumbleTimer = [NSTimer scheduledTimerWithTimeInterval:0.20
|
||||
target:self
|
||||
selector:@selector(rumbleTimer)
|
||||
userInfo:nil
|
||||
repeats:YES];
|
||||
|
||||
Log(LOG_I, @"Number of supported controllers connected: %d", [ControllerSupport getGamepadCount]);
|
||||
Log(LOG_I, @"Multi-controller: %d", _multiController);
|
||||
|
||||
Log(LOG_I, @"Number of controllers connected: %ld", (long)[[GCController controllers] count]);
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
[self assignController:controller];
|
||||
[self registerControllerCallbacks:controller];
|
||||
[self updateAutoOnScreenControlMode];
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
[self assignController:controller];
|
||||
[self registerControllerCallbacks:controller];
|
||||
[self updateAutoOnScreenControlMode];
|
||||
}
|
||||
}
|
||||
|
||||
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||
Log(LOG_I, @"Controller connected!");
|
||||
|
||||
GCController* controller = note.object;
|
||||
|
||||
if (![ControllerSupport isSupportedGamepad:controller]) {
|
||||
// Ignore micro gamepads and motion controllers
|
||||
return;
|
||||
}
|
||||
|
||||
[self assignController:controller];
|
||||
|
||||
// Register callbacks on the new controller
|
||||
@@ -336,26 +553,42 @@
|
||||
Log(LOG_I, @"Controller disconnected!");
|
||||
|
||||
GCController* controller = note.object;
|
||||
|
||||
if (![ControllerSupport isSupportedGamepad:controller]) {
|
||||
// Ignore micro gamepads and motion controllers
|
||||
return;
|
||||
}
|
||||
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
[_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
_controllerNumbers &= ~(1 << controller.playerIndex);
|
||||
self->_controllerNumbers &= ~(1 << controller.playerIndex);
|
||||
Log(LOG_I, @"Unassigning controller index: %ld", (long)controller.playerIndex);
|
||||
|
||||
// Unset the GCController on this object (in case it is the OSC, which will persist)
|
||||
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
limeController.gamepad = nil;
|
||||
|
||||
// Inform the server of the updated active gamepads before removing this controller
|
||||
[self updateFinished:limeController];
|
||||
[self->_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||
|
||||
// Re-evaluate the on-screen control mode
|
||||
[self updateAutoOnScreenControlMode];
|
||||
}];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
-(void) cleanup
|
||||
{
|
||||
[_rumbleTimer invalidate];
|
||||
_rumbleTimer = nil;
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
||||
[_controllers removeAllObjects];
|
||||
_controllerNumbers = 0;
|
||||
for (GCController* controller in [GCController controllers]) {
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||
[self unregisterControllerCallbacks:controller];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// KeyboardSupport.h
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/25/18.
|
||||
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
@interface KeyboardSupport : NSObject
|
||||
|
||||
struct KeyEvent {
|
||||
u_short keycode;
|
||||
u_short modifierKeycode;
|
||||
u_char modifier;
|
||||
};
|
||||
|
||||
+ (struct KeyEvent) translateKeyEvent:(unichar) inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags;
|
||||
|
||||
@end
|
||||
@@ -0,0 +1,191 @@
|
||||
//
|
||||
// KeyboardSupport.m
|
||||
// Moonlight
|
||||
//
|
||||
// Created by Diego Waxemberg on 8/25/18.
|
||||
// Copyright © 2018 Moonlight Game Streaming Project. All rights reserved.
|
||||
//
|
||||
|
||||
#import "KeyboardSupport.h"
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation KeyboardSupport
|
||||
|
||||
+ (struct KeyEvent)translateKeyEvent:(unichar)inputChar withModifierFlags:(UIKeyModifierFlags)modifierFlags {
|
||||
struct KeyEvent event;
|
||||
event.keycode = 0;
|
||||
event.modifier = 0;
|
||||
event.modifierKeycode = 0;
|
||||
|
||||
switch (modifierFlags) {
|
||||
case UIKeyModifierAlphaShift:
|
||||
case UIKeyModifierShift:
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierControl:
|
||||
[KeyboardSupport addControlModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierAlternate:
|
||||
[KeyboardSupport addAltModifier:&event];
|
||||
break;
|
||||
case UIKeyModifierCommand:
|
||||
case UIKeyModifierNumericPad:
|
||||
break;
|
||||
}
|
||||
if (inputChar >= 0x30 && inputChar <= 0x39) {
|
||||
// Numbers 0-9
|
||||
event.keycode = inputChar;
|
||||
} else if (inputChar >= 0x41 && inputChar <= 0x5A) {
|
||||
// Capital letters
|
||||
event.keycode = inputChar;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
} else if (inputChar >= 0x61 && inputChar <= 0x7A) {
|
||||
// Lower case letters
|
||||
event.keycode = inputChar - (0x61 - 0x41);
|
||||
} switch (inputChar) {
|
||||
case ' ': // Spacebar
|
||||
event.keycode = 0x20;
|
||||
break;
|
||||
case '-': // Hyphen '-'
|
||||
event.keycode = 0xBD;
|
||||
break;
|
||||
case '/': // Forward slash '/'
|
||||
event.keycode = 0xBF;
|
||||
break;
|
||||
case ':': // Colon ':'
|
||||
event.keycode = 0xBA;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ';': // Semi-colon ';'
|
||||
event.keycode = 0xBA;
|
||||
break;
|
||||
case '(': // Open parenthesis '('
|
||||
event.keycode = 0x39; // '9'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ')': // Close parenthesis ')'
|
||||
event.keycode = 0x30; // '0'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '$': // Dollar sign '$'
|
||||
event.keycode = 0x34; // '4'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '&': // Ampresand '&'
|
||||
event.keycode = 0x37; // '7'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '@': // At-sign '@'
|
||||
event.keycode = 0x32; // '2'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '"':
|
||||
event.keycode = 0xDE;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '\'':
|
||||
event.keycode = 0xDE;
|
||||
break;
|
||||
case '!':
|
||||
event.keycode = 0x31; // '1'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '?':
|
||||
event.keycode = 0xBF; // '/'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case ',':
|
||||
event.keycode = 0xBC;
|
||||
break;
|
||||
case '<':
|
||||
event.keycode = 0xBC;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '.':
|
||||
event.keycode = 0xBE;
|
||||
break;
|
||||
case '>':
|
||||
event.keycode = 0xBE;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '[':
|
||||
event.keycode = 0xDB;
|
||||
break;
|
||||
case ']':
|
||||
event.keycode = 0xDD;
|
||||
break;
|
||||
case '{':
|
||||
event.keycode = 0xDB;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '}':
|
||||
event.keycode = 0xDD;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '#':
|
||||
event.keycode = 0x33; // '3'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '%':
|
||||
event.keycode = 0x35; // '5'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '^':
|
||||
event.keycode = 0x36; // '6'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '*':
|
||||
event.keycode = 0x38; // '8'
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '+':
|
||||
event.keycode = 0xBB;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '=':
|
||||
event.keycode = 0xBB;
|
||||
break;
|
||||
case '_':
|
||||
event.keycode = 0xBD;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '\\':
|
||||
event.keycode = 0xDC;
|
||||
break;
|
||||
case '|':
|
||||
event.keycode = 0xDC;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '~':
|
||||
event.keycode = 0xC0;
|
||||
[KeyboardSupport addShiftModifier:&event];
|
||||
break;
|
||||
case '`':
|
||||
event.keycode = 0xC0;
|
||||
break;
|
||||
case '\t':
|
||||
event.keycode = 0x09;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
+ (void) addShiftModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_SHIFT;
|
||||
event->modifierKeycode = 0x10;
|
||||
}
|
||||
|
||||
+ (void) addControlModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_CTRL;
|
||||
event->modifierKeycode = 0x11;
|
||||
}
|
||||
|
||||
+ (void) addAltModifier:(struct KeyEvent*)event {
|
||||
event->modifier = MODIFIER_ALT;
|
||||
event->modifierKeycode = 0x12;
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -22,12 +22,15 @@ typedef NS_ENUM(NSInteger, OnScreenControlsLevel) {
|
||||
// Internal levels selected by ControllerSupport
|
||||
OnScreenControlsLevelAutoGCGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepad,
|
||||
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||
};
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
|
||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||
#endif
|
||||
|
||||
@end
|
||||
|
||||
@@ -8,12 +8,9 @@
|
||||
|
||||
#import "OnScreenControls.h"
|
||||
#import "ControllerSupport.h"
|
||||
//#import "Controller.h"
|
||||
#import "Controller.h"
|
||||
#include "Limelight.h"
|
||||
|
||||
#import "Moonlight-Swift.h"
|
||||
@class Controller;
|
||||
|
||||
#define UPDATE_BUTTON(x, y) (buttonFlags = \
|
||||
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
|
||||
|
||||
@@ -123,7 +120,7 @@ static float L3_Y;
|
||||
_edgeDelegate = swipeDelegate;
|
||||
_deadTouches = [[NSMutableArray alloc] init];
|
||||
|
||||
_iPad = (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad);
|
||||
_iPad = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPad);
|
||||
_controlArea = CGRectMake(0, 0, _view.frame.size.width, _view.frame.size.height);
|
||||
if (_iPad)
|
||||
{
|
||||
@@ -202,15 +199,27 @@ static float L3_Y;
|
||||
[self hideSticks];
|
||||
[self drawL3R3];
|
||||
break;
|
||||
case OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons:
|
||||
// This variant of GCExtendedGamepad has L3 and R3 but
|
||||
// is still missing Select
|
||||
[self setupExtendedGamepadControls];
|
||||
|
||||
[self hideButtons];
|
||||
[self hideBumpers];
|
||||
[self hideTriggers];
|
||||
[self hideL3R3];
|
||||
[self drawStartSelect];
|
||||
[self hideSticks];
|
||||
break;
|
||||
case OnScreenControlsLevelSimple:
|
||||
[self setupSimpleControls];
|
||||
|
||||
[self drawTriggers];
|
||||
[self drawStartSelect];
|
||||
[self drawL3R3];
|
||||
[self hideButtons];
|
||||
[self hideTriggers];
|
||||
[self hideL3R3];
|
||||
[self hideBumpers];
|
||||
[self hideSticks];
|
||||
[self drawStartSelect];
|
||||
[self drawButtons];
|
||||
break;
|
||||
case OnScreenControlsLevelFull:
|
||||
[self setupComplexControls];
|
||||
@@ -238,8 +247,22 @@ static float L3_Y;
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
|
||||
R3_Y = _controlArea.size.height * .85 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
L3_X = _controlArea.size.width * .15 + _controlArea.origin.x;
|
||||
R3_X = _controlArea.size.width * .85 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
||||
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
// For GCGamepad controls we move triggers, start, and select
|
||||
@@ -248,41 +271,21 @@ static float L3_Y;
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
L2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
|
||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
R2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
|
||||
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
|
||||
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
|
||||
SELECT_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .95 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
START_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
SELECT_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
// The analog sticks are kept closer to the sides on iPad
|
||||
LS_CENTER_X = _controlArea.size.width * .18 + _controlArea.origin.x;
|
||||
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
RS_CENTER_X = _controlArea.size.width * .82 + _controlArea.origin.x;
|
||||
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
} else {
|
||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
LS_CENTER_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
||||
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
RS_CENTER_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
||||
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
LS_CENTER_X = _controlArea.size.width * .15 + _controlArea.origin.x;
|
||||
RS_CENTER_X = _controlArea.size.width * .85 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,10 +294,7 @@ static float L3_Y;
|
||||
// Start with the default complex layout
|
||||
[self setupComplexControls];
|
||||
|
||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
@@ -302,6 +302,20 @@ static float L3_Y;
|
||||
|
||||
R2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
|
||||
if (_iPad) {
|
||||
// Lower the D-pad and buttons on iPad
|
||||
D_PAD_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
BUTTON_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
|
||||
// Move Start and Select closer to sides
|
||||
SELECT_X = _controlArea.size.width * .2 + _controlArea.origin.x;
|
||||
START_X = _controlArea.size.width * .8 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) setupComplexControls
|
||||
@@ -315,9 +329,9 @@ static float L3_Y;
|
||||
{
|
||||
// The analog sticks are kept closer to the sides on iPad
|
||||
LS_CENTER_X = _controlArea.size.width * .22 + _controlArea.origin.x;
|
||||
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
LS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
|
||||
RS_CENTER_X = _controlArea.size.width * .77 + _controlArea.origin.x;
|
||||
RS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
RS_CENTER_Y = _controlArea.size.height * .80 + _controlArea.origin.y;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -332,18 +346,24 @@ static float L3_Y;
|
||||
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||
|
||||
L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
L1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
|
||||
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
L2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
|
||||
R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
R1_Y = _controlArea.size.height * .27 + _controlArea.origin.y;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
R2_Y = _controlArea.size.height * .1 + _controlArea.origin.y;
|
||||
L3_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
||||
L3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
||||
R3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||
|
||||
if (_iPad) {
|
||||
// Move L/R buttons closer to the side on iPad
|
||||
L1_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||
R1_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
R2_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||
}
|
||||
else {
|
||||
L1_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
L2_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||
R1_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||
}
|
||||
}
|
||||
|
||||
- (void) drawButtons {
|
||||
@@ -836,26 +856,31 @@ static float L3_Y;
|
||||
}
|
||||
|
||||
- (BOOL) isInDeadZone:(UITouch*) touch {
|
||||
switch (_level) {
|
||||
case OnScreenControlsLevelFull:
|
||||
return [self isDpadDeadZone:touch]
|
||||
|| [self isAbxyDeadZone:touch]
|
||||
|| [self isTriggerDeadZone:touch]
|
||||
|| [self isBumperDeadZone:touch]
|
||||
|| [self isStartSelectDeadZone:touch];
|
||||
case OnScreenControlsLevelSimple:
|
||||
return [self isTriggerDeadZone:touch]
|
||||
|| [self isStartSelectDeadZone:touch]
|
||||
|| [self isL3R3DeadZone:touch];
|
||||
case OnScreenControlsLevelAutoGCExtendedGamepad:
|
||||
return [self isL3R3DeadZone:touch]
|
||||
|| [self isStartSelectDeadZone:touch];
|
||||
case OnScreenControlsLevelAutoGCGamepad:
|
||||
return [self isTriggerDeadZone:touch]
|
||||
|| [self isStartSelectDeadZone:touch];
|
||||
default:
|
||||
return false;
|
||||
// Dynamically evaluate deadzones based on the controls
|
||||
// on screen at the time
|
||||
if (_leftButton.superlayer != nil && [self isDpadDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_aButton.superlayer != nil && [self isAbxyDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l2Button.superlayer != nil && [self isTriggerDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l1Button.superlayer != nil && [self isBumperDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_startButton.superlayer != nil && [self isStartSelectDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_l3Button.superlayer != nil && [self isL3R3DeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
else if (_leftStickBackground.superlayer != nil && [self isStickDeadZone:touch]) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
- (BOOL) isDpadDeadZone:(UITouch*) touch {
|
||||
@@ -926,6 +951,19 @@ static float L3_Y;
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isStickDeadZone:(UITouch*) touch {
|
||||
return [self isDeadZone:touch
|
||||
startX:_leftStickBackground.frame.origin.x - 15
|
||||
startY:_leftStickBackground.frame.origin.y - 15
|
||||
endX:_leftStickBackground.frame.origin.x + _leftStickBackground.frame.size.width + 15
|
||||
endY:_view.frame.origin.y + _view.frame.size.height]
|
||||
|| [self isDeadZone:touch
|
||||
startX:_rightStickBackground.frame.origin.x - 15
|
||||
startY:_rightStickBackground.frame.origin.y - 15
|
||||
endX:_rightStickBackground.frame.origin.x + _rightStickBackground.frame.size.width + 15
|
||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
||||
}
|
||||
|
||||
- (BOOL) isDeadZone:(UITouch*) touch startX:(float)deadZoneStartX startY:(float)deadZoneStartY endX:(float)deadZoneEndX endY:(float)deadZoneEndY {
|
||||
deadZoneStartX -= DEAD_ZONE_PADDING;
|
||||
deadZoneStartY -= DEAD_ZONE_PADDING;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <UIKit/UIKit.h>
|
||||
#import "ControllerSupport.h"
|
||||
|
||||
@protocol EdgeDetectionDelegate <NSObject>
|
||||
@@ -15,8 +14,11 @@
|
||||
|
||||
@end
|
||||
|
||||
@interface StreamView : UIView
|
||||
@interface StreamView : OSView <UITextFieldDelegate>
|
||||
|
||||
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
|
||||
|
||||
- (void) setupStreamView;
|
||||
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
|
||||
- (void) setMouseDeltaFactors:(float)x y:(float)y;
|
||||
|
||||
|
||||
@@ -11,18 +11,27 @@
|
||||
#import "OnScreenControls.h"
|
||||
#import "DataManager.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "KeyboardSupport.h"
|
||||
|
||||
@implementation StreamView {
|
||||
CGPoint touchLocation, originalLocation;
|
||||
BOOL touchMoved;
|
||||
OnScreenControls* onScreenControls;
|
||||
|
||||
BOOL isInputingText;
|
||||
BOOL isDragging;
|
||||
NSTimer* dragTimer;
|
||||
|
||||
float xDeltaFactor;
|
||||
float yDeltaFactor;
|
||||
float screenFactor;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UIGestureRecognizer* remotePressRecognizer;
|
||||
UIGestureRecognizer* remoteLongPressRecognizer;
|
||||
#endif
|
||||
|
||||
NSDictionary<NSString *, NSNumber *> *dictCodes;
|
||||
}
|
||||
|
||||
- (void) setMouseDeltaFactors:(float)x y:(float)y {
|
||||
@@ -32,6 +41,19 @@
|
||||
screenFactor = [[UIScreen mainScreen] scale];
|
||||
}
|
||||
|
||||
- (void) setupStreamView {
|
||||
#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 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];
|
||||
@@ -44,11 +66,12 @@
|
||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
||||
[onScreenControls setLevel:level];
|
||||
}
|
||||
[self becomeFirstResponder];
|
||||
}
|
||||
|
||||
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||
// Movements of greater than 20 pixels are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 20;
|
||||
// Movements of greater than 10 pixels are considered confirmed
|
||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10;
|
||||
}
|
||||
|
||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
@@ -76,8 +99,6 @@
|
||||
|
||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if ([[event allTouches] count] == 1) {
|
||||
UITouch *touch = [[event allTouches] anyObject];
|
||||
CGPoint currentLocation = [touch locationInView:self];
|
||||
@@ -128,33 +149,54 @@
|
||||
if (![onScreenControls handleTouchUpEvent:touches]) {
|
||||
[dragTimer invalidate];
|
||||
dragTimer = nil;
|
||||
if (!touchMoved) {
|
||||
if ([[event allTouches] count] == 2) {
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
} else {
|
||||
if (!isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
} else if (!touchMoved) {
|
||||
if ([[event allTouches] count] == 3) {
|
||||
if (isInputingText) {
|
||||
Log(LOG_D, @"Closing the keyboard");
|
||||
[_keyInputField resignFirstResponder];
|
||||
isInputingText = false;
|
||||
} else {
|
||||
Log(LOG_D, @"Opening the keyboard");
|
||||
// Prepare the textbox used to capture keyboard events.
|
||||
_keyInputField.delegate = self;
|
||||
_keyInputField.text = @"0";
|
||||
[_keyInputField becomeFirstResponder];
|
||||
[_keyInputField addTarget:self action:@selector(onKeyboardPressed:) forControlEvents:UIControlEventEditingChanged];
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
// Undo causes issues for our state management, so turn it off
|
||||
[_keyInputField.undoManager disableUndoRegistration];
|
||||
|
||||
isInputingText = true;
|
||||
}
|
||||
} else if ([[event allTouches] count] == 2) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
Log(LOG_D, @"Sending right mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||
});
|
||||
} else if ([[event allTouches] count] == 1) {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
if (!self->isDragging){
|
||||
Log(LOG_D, @"Sending left mouse button press");
|
||||
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
|
||||
// Wait 100 ms to simulate a real button press
|
||||
usleep(100 * 1000);
|
||||
}
|
||||
self->isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
});
|
||||
}
|
||||
}
|
||||
else if (isDragging) {
|
||||
isDragging = false;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
||||
}
|
||||
|
||||
// We we're moving from 2+ touches to 1. Synchronize the current position
|
||||
// of the active finger so we don't jump unexpectedly on the next touchesMoved
|
||||
@@ -175,5 +217,147 @@
|
||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)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");
|
||||
|
||||
isDragging = true;
|
||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
||||
}
|
||||
#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;
|
||||
}
|
||||
|
||||
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||
// This method is called when the "Return" key is pressed.
|
||||
LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
|
||||
usleep(50 * 1000);
|
||||
LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (void)onKeyboardPressed:(UITextField *)textField {
|
||||
NSString* inputText = textField.text;
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
// If the text became empty, we know the user pressed the backspace key.
|
||||
if ([inputText isEqual:@""]) {
|
||||
LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
|
||||
usleep(50 * 1000);
|
||||
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
|
||||
} else {
|
||||
// Character 0 will be our known sentinel value
|
||||
for (int i = 1; i < [inputText length]; i++) {
|
||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
||||
if (event.keycode == 0) {
|
||||
// If we don't know the code, don't send anything.
|
||||
Log(LOG_W, @"Unknown key code: [%c]", [inputText characterAtIndex:i]);
|
||||
continue;
|
||||
}
|
||||
[self sendLowLevelEvent:event];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Reset text field back to known state
|
||||
textField.text = @"0";
|
||||
|
||||
// Move the insertion point back to the end of the text box
|
||||
UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
|
||||
[textField setSelectedTextRange:textRange];
|
||||
}
|
||||
|
||||
- (void)specialCharPressed:(UIKeyCommand *)cmd {
|
||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
|
||||
event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
|
||||
[self sendLowLevelEvent:event];
|
||||
}
|
||||
|
||||
- (void)keyPressed:(UIKeyCommand *)cmd {
|
||||
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
|
||||
[self sendLowLevelEvent:event];
|
||||
}
|
||||
|
||||
- (void)sendLowLevelEvent:(struct KeyEvent)event {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||
// When we want to send a modified key (like uppercase letters) we need to send the
|
||||
// modifier ("shift") seperately from the key itself.
|
||||
if (event.modifier != 0) {
|
||||
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
|
||||
}
|
||||
LiSendKeyboardEvent(event.keycode, KEY_ACTION_DOWN, event.modifier);
|
||||
usleep(50 * 1000);
|
||||
LiSendKeyboardEvent(event.keycode, KEY_ACTION_UP, event.modifier);
|
||||
if (event.modifier != 0) {
|
||||
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (BOOL)canBecomeFirstResponder {
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSArray<UIKeyCommand *> *)keyCommands
|
||||
{
|
||||
NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
|
||||
|
||||
NSMutableArray<UIKeyCommand *> * commands = [NSMutableArray<UIKeyCommand *> array];
|
||||
dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
|
||||
|
||||
[charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
|
||||
options:NSStringEnumerationByComposedCharacterSequences
|
||||
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
|
||||
}];
|
||||
|
||||
for (NSString *c in [dictCodes keyEnumerator]) {
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:0
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierShift
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierShift | UIKeyModifierControl
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierControl
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
|
||||
action:@selector(specialCharPressed:)]];
|
||||
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||
modifierFlags:UIKeyModifierAlternate
|
||||
action:@selector(specialCharPressed:)]];
|
||||
}
|
||||
|
||||
return commands;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CADisableMinimumFrameDuration</key>
|
||||
<true/>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
@@ -17,11 +19,24 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.4.0</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleSignature</key>
|
||||
<string>????</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>2</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>GCSupportedGameControllers</key>
|
||||
<array>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>ExtendedGamepad</string>
|
||||
</dict>
|
||||
<dict>
|
||||
<key>ProfileName</key>
|
||||
<string>Gamepad</string>
|
||||
</dict>
|
||||
</array>
|
||||
<key>GCSupportsControllerUserInteraction</key>
|
||||
<true/>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.games</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
@@ -31,15 +46,6 @@
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>UIAppFonts</key>
|
||||
<array>
|
||||
<string>Roboto-Black.ttf</string>
|
||||
<string>Roboto-Bold.ttf</string>
|
||||
<string>Roboto-Light.ttf</string>
|
||||
<string>Roboto-Medium.ttf</string>
|
||||
<string>Roboto-Regular.ttf</string>
|
||||
<string>Roboto-Thin.ttf</string>
|
||||
</array>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>Launch Screen</string>
|
||||
<key>UIMainStoryboardFile</key>
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
//
|
||||
|
||||
#import <Availability.h>
|
||||
#include <TargetConditionals.h>
|
||||
|
||||
#ifndef __IPHONE_3_0
|
||||
#warning "This project uses features only available in iOS SDK 3.0 and later."
|
||||
#endif
|
||||
|
||||
#ifdef __OBJC__
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
#import <UIKit/UIKit.h>
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "Logger.h"
|
||||
#endif
|
||||
#elif TARGET_OS_MAC
|
||||
#import <AppKit/AppKit.h>
|
||||
#endif
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <CoreData/CoreData.h>
|
||||
#import "Logger.h"
|
||||
#include "OSPortabilityDefs.h"
|
||||
#endif
|
||||
|
||||
@@ -3,6 +3,6 @@
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>_XCCurrentVersionName</key>
|
||||
<string>Moonlight v1.0-2.xcdatamodel</string>
|
||||
<string>Moonlight v1.4.xcdatamodel</string>
|
||||
</dict>
|
||||
</plist>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model userDefinedModelVersionIdentifier="" type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="9057" systemVersion="15B42" minimumToolsVersion="Automatic">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="13772" systemVersion="17D102" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES">
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
||||
@@ -12,21 +12,22 @@
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" syncable="YES"/>
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="135"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="150"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17F77" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,39 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14135" systemVersion="17G65" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="165"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="18C54" minimumToolsVersion="Xcode 7.3" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="">
|
||||
<entity name="App" representedClassName="App" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="hdrSupported" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<relationship name="host" maxCount="1" deletionRule="Nullify" destinationEntity="Host" inverseName="appList" inverseEntity="Host" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Host" representedClassName="Host" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="address" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="externalAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="name" attributeType="String" syncable="YES"/>
|
||||
<attribute name="pairState" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="serverCert" optional="YES" attributeType="Binary" syncable="YES"/>
|
||||
<attribute name="serverCodecModeSupport" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uuid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||
</entity>
|
||||
<entity name="Settings" representedClassName="Settings" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="multiController" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="optimizeGames" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="playAudioOnPC" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="streamingRemotely" attributeType="Boolean" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="uniqueId" attributeType="String" syncable="YES"/>
|
||||
<attribute name="useHevc" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="width" attributeType="Integer 32" defaultValueString="1280" usesScalarValueType="NO" syncable="YES"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||
<element name="Host" positionX="0" positionY="0" width="128" height="195"/>
|
||||
<element name="Settings" positionX="0" positionY="0" width="128" height="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -0,0 +1,41 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14490.99" systemVersion="18F132" 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="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="225"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TemporaryApp.h"
|
||||
#import "HttpManager.h"
|
||||
#import "TemporaryHost.h"
|
||||
@@ -22,5 +21,6 @@
|
||||
- (id) initWithCallback:(id<AppAssetCallback>)callback;
|
||||
- (void) retrieveAssetsFromHost:(TemporaryHost*)host;
|
||||
- (void) stopRetrieving;
|
||||
+ (NSString*) boxArtPathForApp:(TemporaryApp*)app;
|
||||
|
||||
@end
|
||||
|
||||
@@ -19,6 +19,22 @@
|
||||
|
||||
static const int MAX_REQUEST_COUNT = 4;
|
||||
|
||||
+ (NSString*) boxArtPathForApp:(TemporaryApp*)app {
|
||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES);
|
||||
NSString *filePath = [paths objectAtIndex:0];
|
||||
|
||||
// Keep app assets separate by host UUID
|
||||
filePath = [filePath stringByAppendingPathComponent:app.host.uuid];
|
||||
|
||||
// Use the app ID as the file name
|
||||
filePath = [filePath stringByAppendingPathComponent:app.id];
|
||||
|
||||
// Add a png extension
|
||||
filePath = [filePath stringByAppendingPathExtension:@"png"];
|
||||
|
||||
return filePath;
|
||||
}
|
||||
|
||||
- (id) initWithCallback:(id<AppAssetCallback>)callback {
|
||||
self = [super init];
|
||||
_callback = callback;
|
||||
@@ -30,7 +46,7 @@ static const int MAX_REQUEST_COUNT = 4;
|
||||
|
||||
- (void) retrieveAssetsFromHost:(TemporaryHost*)host {
|
||||
for (TemporaryApp* app in host.appList) {
|
||||
if (app.image == nil) {
|
||||
if (![[NSFileManager defaultManager] fileExistsAtPath:[AppAssetManager boxArtPathForApp:app]]) {
|
||||
AppAssetRetriever* retriever = [[AppAssetRetriever alloc] init];
|
||||
retriever.app = app;
|
||||
retriever.host = host;
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpResponse.h"
|
||||
|
||||
@interface AppAssetResponse : NSObject <Response>
|
||||
|
||||
@@ -16,9 +16,12 @@
|
||||
self.statusMessage = @"App asset has no status message";
|
||||
self.statusCode = -1;
|
||||
}
|
||||
|
||||
- (UIImage*) getImage {
|
||||
UIImage* appImage = [[UIImage alloc] initWithData:self.data];
|
||||
- (OSImage*) getImage {
|
||||
#if TARGET_OS_IPHONE
|
||||
OSImage* appImage = [[OSImage alloc] initWithData:self.data];
|
||||
#else
|
||||
OSImage* appImage = nil;
|
||||
#endif
|
||||
return appImage;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TemporaryHost.h"
|
||||
#import "TemporaryApp.h"
|
||||
#import "AppAssetManager.h"
|
||||
|
||||
@@ -18,18 +18,24 @@ static const double RETRY_DELAY = 2; // seconds
|
||||
static const int MAX_ATTEMPTS = 5;
|
||||
|
||||
- (void) main {
|
||||
UIImage* appImage = nil;
|
||||
int attempts = 0;
|
||||
while (![self isCancelled] && appImage == nil && attempts++ < MAX_ATTEMPTS) {
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] deviceName:deviceName cert:[CryptoManager readCertFromFile]];
|
||||
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] serverCert:_host.serverCert];
|
||||
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
|
||||
|
||||
#if TARGET_OS_IPHONE
|
||||
if (appAssetResp.data != nil) {
|
||||
NSString* boxArtPath = [AppAssetManager boxArtPathForApp:self.app];
|
||||
[[NSFileManager defaultManager] createDirectoryAtPath:[boxArtPath stringByDeletingLastPathComponent] withIntermediateDirectories:YES attributes:nil error:nil];
|
||||
[appAssetResp.data writeToFile:boxArtPath atomically:NO];
|
||||
break;
|
||||
}
|
||||
#else
|
||||
|
||||
appImage = [UIImage imageWithData:appAssetResp.data];
|
||||
self.app.image = UIImagePNGRepresentation(appImage);
|
||||
#endif
|
||||
|
||||
if (![self isCancelled] && appImage == nil) {
|
||||
if (![self isCancelled]) {
|
||||
[NSThread sleepForTimeInterval:RETRY_DELAY];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
static const char* TAG_APP = "App";
|
||||
static const char* TAG_APP_TITLE = "AppTitle";
|
||||
static const char* TAG_APP_ID = "ID";
|
||||
static const char* TAG_HDR_SUPPORTED = "IsHdrSupported";
|
||||
static const char* TAG_APP_INSTALL_PATH = "AppInstallPath";
|
||||
|
||||
- (void)populateWithData:(NSData *)xml {
|
||||
self.data = xml;
|
||||
@@ -66,14 +68,14 @@ static const char* TAG_APP_ID = "ID";
|
||||
xmlNodePtr appInfoNode = node->xmlChildrenNode;
|
||||
NSString* appName = @"";
|
||||
NSString* appId = nil;
|
||||
NSString* hdrSupported = @"0";
|
||||
NSString* appInstallPath = nil;
|
||||
while (appInfoNode != NULL) {
|
||||
if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_TITLE)) {
|
||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||
if (nodeVal != NULL) {
|
||||
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||
xmlFree(nodeVal);
|
||||
} else {
|
||||
appName = @"";
|
||||
}
|
||||
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_ID)) {
|
||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||
@@ -81,13 +83,28 @@ static const char* TAG_APP_ID = "ID";
|
||||
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||
xmlFree(nodeVal);
|
||||
}
|
||||
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_HDR_SUPPORTED)) {
|
||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||
if (nodeVal != NULL) {
|
||||
hdrSupported = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||
xmlFree(nodeVal);
|
||||
}
|
||||
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_INSTALL_PATH)) {
|
||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||
if (nodeVal != NULL) {
|
||||
appInstallPath = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||
xmlFree(nodeVal);
|
||||
}
|
||||
}
|
||||
|
||||
appInfoNode = appInfoNode->next;
|
||||
}
|
||||
if (appId != nil) {
|
||||
TemporaryApp* app = [[TemporaryApp alloc] init];
|
||||
app.name = appName;
|
||||
app.id = appId;
|
||||
app.hdrSupported = [hdrSupported intValue] != 0;
|
||||
app.installPath = appInstallPath;
|
||||
[_appList addObject:app];
|
||||
}
|
||||
}
|
||||
@@ -95,6 +112,40 @@ static const char* TAG_APP_ID = "ID";
|
||||
}
|
||||
|
||||
xmlFreeDoc(docPtr);
|
||||
|
||||
// APP STORE REVIEW COMPLIANCE
|
||||
//
|
||||
// Remove default Steam entry from the app list to comply with Apple App Store Guideline 4.2.7d:
|
||||
//
|
||||
// The UI appearing on the client does not resemble an iOS or App Store view, does not provide a store-like interface,
|
||||
// or include the ability to browse, select, or purchase software not already owned or licensed by the user.
|
||||
//
|
||||
// However, if the user manually adds Steam themselves, then we will display it.
|
||||
TemporaryApp* officialSteamApp = nil;
|
||||
TemporaryApp* manuallyAddedSteamApp = nil;
|
||||
for (TemporaryApp* app in _appList) {
|
||||
if (app.installPath != nil && [[app.installPath lowercaseString] hasSuffix:@"\\steam\\"]) {
|
||||
// The official Steam app is marked as HDR supported, while manually added ones are not.
|
||||
if ([app.name isEqualToString:@"Steam"] && app.hdrSupported) {
|
||||
officialSteamApp = app;
|
||||
}
|
||||
else {
|
||||
manuallyAddedSteamApp = app;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// To be safe, don't do anything if we didn't find an HDR-enabled Steam app.
|
||||
if (officialSteamApp != nil) {
|
||||
// If we didn't find a manually added Steam app, remove the official one to
|
||||
// comply with the App Store guidelines.
|
||||
if (manuallyAddedSteamApp == nil) {
|
||||
[_appList removeObject:officialSteamApp];
|
||||
}
|
||||
else {
|
||||
[_appList removeObject:manuallyAddedSteamApp];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSSet*) getAppList {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
//
|
||||
// ConnectionHelper.h
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 22.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import "AppListResponse.h"
|
||||
|
||||
#ifndef ConnectionHelper_h
|
||||
#define ConnectionHelper_h
|
||||
|
||||
|
||||
@interface ConnectionHelper : NSObject
|
||||
|
||||
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) serverCert uniqueID:(NSString*) uniqueId;
|
||||
|
||||
@end
|
||||
|
||||
#endif /* ConnectionHelper_h */
|
||||
@@ -0,0 +1,40 @@
|
||||
//
|
||||
// ConnectionHelper.m
|
||||
// Moonlight macOS
|
||||
//
|
||||
// Created by Felix Kratz on 22.03.18.
|
||||
// Copyright © 2018 Felix Kratz. All rights reserved.
|
||||
//
|
||||
|
||||
#import "ConnectionHelper.h"
|
||||
#import "ServerInfoResponse.h"
|
||||
#import "HttpManager.h"
|
||||
#import "PairManager.h"
|
||||
#import "DiscoveryManager.h"
|
||||
|
||||
@implementation ConnectionHelper
|
||||
|
||||
+(AppListResponse*) getAppListForHostWithHostIP:(NSString*) hostIP serverCert:(NSData*) cert uniqueID:(NSString*) uniqueId {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostIP uniqueId:uniqueId serverCert:cert];
|
||||
|
||||
// Try up to 5 times to get the app list
|
||||
AppListResponse* appListResp;
|
||||
for (int i = 0; i < 5; i++) {
|
||||
appListResp = [[AppListResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appListResp withUrlRequest:[hMan newAppListRequest]]];
|
||||
if (appListResp == nil || ![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
||||
Log(LOG_W, @"Failed to get applist on try %d: %@", i, appListResp.statusMessage);
|
||||
|
||||
// Wait for one second then retry
|
||||
[NSThread sleepForTimeInterval:1];
|
||||
}
|
||||
else {
|
||||
Log(LOG_I, @"App list successfully retreived - took %d tries", i);
|
||||
return appListResp;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "MDNSManager.h"
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
|
||||
@@ -41,25 +41,25 @@
|
||||
|
||||
_opQueue = [[NSOperationQueue alloc] init];
|
||||
_mdnsMan = [[MDNSManager alloc] initWithCallback:self];
|
||||
[CryptoManager generateKeyPairUsingSSl];
|
||||
[CryptoManager generateKeyPairUsingSSL];
|
||||
_uniqueId = [IdManager getUniqueId];
|
||||
_cert = [CryptoManager readCertFromFile];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId deviceName:deviceName cert:_cert];
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:hostAddress uniqueId:_uniqueId serverCert:nil];
|
||||
ServerInfoResponse* serverInfoResponse = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResponse withUrlRequest:[hMan newServerInfoRequest:false] fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
|
||||
TemporaryHost* host = nil;
|
||||
if ([serverInfoResponse isStatusOk]) {
|
||||
host = [[TemporaryHost alloc] init];
|
||||
host.activeAddress = host.address = hostAddress;
|
||||
host.online = YES;
|
||||
host.state = StateOnline;
|
||||
[serverInfoResponse populateHost:host];
|
||||
if (![self addHostToDiscovery:host]) {
|
||||
callback(nil, @"Host already added");
|
||||
callback(nil, @"Host information updated");
|
||||
} else {
|
||||
callback(host, nil);
|
||||
}
|
||||
@@ -70,6 +70,10 @@
|
||||
}
|
||||
|
||||
- (void) startDiscovery {
|
||||
if (shouldDiscover) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Starting discovery");
|
||||
shouldDiscover = YES;
|
||||
[_mdnsMan searchForHosts];
|
||||
@@ -79,6 +83,10 @@
|
||||
}
|
||||
|
||||
- (void) stopDiscovery {
|
||||
if (!shouldDiscover) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Stopping discovery");
|
||||
shouldDiscover = NO;
|
||||
[_mdnsMan stopSearching];
|
||||
@@ -87,10 +95,18 @@
|
||||
|
||||
- (void) stopDiscoveryBlocking {
|
||||
Log(LOG_I, @"Stopping discovery and waiting for workers to stop");
|
||||
shouldDiscover = NO;
|
||||
[_mdnsMan stopSearching];
|
||||
[_opQueue cancelAllOperations];
|
||||
|
||||
if (shouldDiscover) {
|
||||
shouldDiscover = NO;
|
||||
[_mdnsMan stopSearching];
|
||||
[_opQueue cancelAllOperations];
|
||||
}
|
||||
|
||||
// Ensure we always wait, just in case discovery
|
||||
// was stopped already but in an async manner that
|
||||
// left operations in progress.
|
||||
[_opQueue waitUntilAllOperationsAreFinished];
|
||||
|
||||
Log(LOG_I, @"All discovery workers stopped");
|
||||
}
|
||||
|
||||
@@ -101,6 +117,12 @@
|
||||
|
||||
TemporaryHost *existingHost = [self getHostInDiscovery:host.uuid];
|
||||
if (existingHost != nil) {
|
||||
// NB: Our logic here depends on the fact that we never propagate
|
||||
// the entire TemporaryHost to existingHost. In particular, when mDNS
|
||||
// discovers a PC and we poll it, we will do so over HTTP which will
|
||||
// not have accurate pair state. The fields explicitly copied below
|
||||
// are accurate though.
|
||||
|
||||
// Update address of existing host
|
||||
if (host.address != nil) {
|
||||
existingHost.address = host.address;
|
||||
@@ -108,23 +130,17 @@
|
||||
if (host.localAddress != nil) {
|
||||
existingHost.localAddress = host.localAddress;
|
||||
}
|
||||
if (host.ipv6Address != nil) {
|
||||
existingHost.ipv6Address = host.ipv6Address;
|
||||
}
|
||||
if (host.externalAddress != nil) {
|
||||
existingHost.externalAddress = host.externalAddress;
|
||||
}
|
||||
existingHost.activeAddress = host.activeAddress;
|
||||
existingHost.state = host.state;
|
||||
return NO;
|
||||
}
|
||||
else {
|
||||
// If we were added without an explicit address,
|
||||
// populate it from our other available addresses
|
||||
if (host.address == nil) {
|
||||
if (host.externalAddress != nil) {
|
||||
host.address = host.externalAddress;
|
||||
}
|
||||
else {
|
||||
host.address = host.localAddress;
|
||||
}
|
||||
}
|
||||
[_hostQueue addObject:host];
|
||||
if (shouldDiscover) {
|
||||
[_opQueue addOperation:[self createWorkerForHost:host]];
|
||||
@@ -142,28 +158,23 @@
|
||||
[_hostQueue removeObject:host];
|
||||
}
|
||||
|
||||
// Override from MDNSCallback
|
||||
- (void)updateHosts:(NSArray *)hosts {
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
// Discover the hosts before adding to eliminate duplicates
|
||||
for (TemporaryHost* host in hosts) {
|
||||
Log(LOG_I, @"Found host through MDNS: %@:", host.name);
|
||||
// Since this is on a background thread, we do not need to use the opQueue
|
||||
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
|
||||
[worker discoverHost];
|
||||
if ([self addHostToDiscovery:host]) {
|
||||
Log(LOG_I, @"Adding host to discovery: %@", host.name);
|
||||
[_callback updateAllHosts:_hostQueue];
|
||||
} else {
|
||||
Log(LOG_I, @"Not adding host to discovery: %@", host.name);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Override from MDNSCallback - called in a worker thread
|
||||
- (void)updateHost:(TemporaryHost*)host {
|
||||
// Discover the hosts before adding to eliminate duplicates
|
||||
Log(LOG_D, @"Found host through MDNS: %@:", host.name);
|
||||
// Since this is on a background thread, we do not need to use the opQueue
|
||||
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
|
||||
[worker discoverHost];
|
||||
if ([self addHostToDiscovery:host]) {
|
||||
Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
|
||||
[self->_callback updateAllHosts:self->_hostQueue];
|
||||
} else {
|
||||
Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
|
||||
}
|
||||
}
|
||||
|
||||
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
|
||||
for (int i = 0; i < _hostQueue.count; i++) {
|
||||
TemporaryHost* discoveredHost = [_hostQueue objectAtIndex:i];
|
||||
for (TemporaryHost* discoveredHost in _hostQueue) {
|
||||
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
|
||||
return discoveredHost;
|
||||
}
|
||||
@@ -172,7 +183,7 @@
|
||||
}
|
||||
|
||||
- (NSOperation*) createWorkerForHost:(TemporaryHost*)host {
|
||||
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId cert:_cert];
|
||||
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId];
|
||||
return worker;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,12 +6,11 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
@interface DiscoveryWorker : NSOperation
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert;
|
||||
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId;
|
||||
- (void) discoverHost;
|
||||
- (TemporaryHost*) getHost;
|
||||
|
||||
|
||||
@@ -16,16 +16,14 @@
|
||||
@implementation DiscoveryWorker {
|
||||
TemporaryHost* _host;
|
||||
NSString* _uniqueId;
|
||||
NSData* _cert;
|
||||
}
|
||||
|
||||
static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
|
||||
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert {
|
||||
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId {
|
||||
self = [super init];
|
||||
_host = host;
|
||||
_uniqueId = uniqueId;
|
||||
_cert = cert;
|
||||
return self;
|
||||
}
|
||||
|
||||
@@ -48,11 +46,14 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
if (_host.localAddress != nil) {
|
||||
[array addObject:_host.localAddress];
|
||||
}
|
||||
if (_host.address != nil) {
|
||||
[array addObject:_host.address];
|
||||
}
|
||||
if (_host.externalAddress != nil) {
|
||||
[array addObject:_host.externalAddress];
|
||||
}
|
||||
if (_host.address != nil) {
|
||||
[array addObject:_host.address];
|
||||
if (_host.ipv6Address != nil) {
|
||||
[array addObject:_host.ipv6Address];
|
||||
}
|
||||
|
||||
// Remove duplicate addresses from the list.
|
||||
@@ -88,8 +89,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
|
||||
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
|
||||
|
||||
// Give the PC 3 tries to respond before declaring it offline
|
||||
for (int i = 0; i < 3; i++) {
|
||||
// Give the PC 2 tries to respond before declaring it offline
|
||||
for (int i = 0; i < 2; i++) {
|
||||
for (NSString *address in addresses) {
|
||||
if (self.cancelled) {
|
||||
// Get out without updating the status because
|
||||
@@ -98,7 +99,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
return;
|
||||
}
|
||||
|
||||
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address];
|
||||
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
|
||||
receivedResponse = [self checkResponse:serverInfoResp];
|
||||
if (receivedResponse) {
|
||||
[serverInfoResp populateHost:_host];
|
||||
@@ -112,29 +113,24 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
||||
}
|
||||
|
||||
if (receivedResponse) {
|
||||
Log(LOG_I, @"Received serverinfo response on try %d", i);
|
||||
Log(LOG_D, @"Received serverinfo response on try %d", i);
|
||||
break;
|
||||
}
|
||||
else {
|
||||
// Wait for one second then retry
|
||||
[NSThread sleepForTimeInterval:1];
|
||||
}
|
||||
}
|
||||
|
||||
_host.online = receivedResponse;
|
||||
_host.state = receivedResponse ? StateOnline : StateOffline;
|
||||
if (receivedResponse) {
|
||||
Log(LOG_D, @"Received response from: %@\n{\n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \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.uuid, _host.mac, _host.pairState, _host.online, _host.activeAddress);
|
||||
Log(LOG_D, @"Received response from: %@\n{\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);
|
||||
}
|
||||
}
|
||||
|
||||
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address {
|
||||
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
|
||||
uniqueId:_uniqueId
|
||||
deviceName:deviceName
|
||||
cert:_cert];
|
||||
serverCert:cert];
|
||||
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
|
||||
withUrlRequest:[hMan newServerInfoRequest]
|
||||
withUrlRequest:[hMan newServerInfoRequest:true]
|
||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
return response;
|
||||
}
|
||||
|
||||
@@ -6,24 +6,26 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpResponse.h"
|
||||
#import "HttpRequest.h"
|
||||
#import "StreamConfiguration.h"
|
||||
|
||||
@interface HttpManager : NSObject <NSURLSessionDelegate>
|
||||
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert;
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt;
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
|
||||
- (void) setServerCert:(NSData*) serverCert;
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
|
||||
- (NSURLRequest*) newUnpairRequest;
|
||||
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge;
|
||||
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp;
|
||||
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret;
|
||||
- (NSURLRequest*) newPairChallenge;
|
||||
- (NSURLRequest*) newAppListRequest;
|
||||
- (NSURLRequest*) newServerInfoRequest;
|
||||
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
|
||||
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
|
||||
- (NSURLRequest*) newHttpServerInfoRequest;
|
||||
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid;
|
||||
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId;
|
||||
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
|
||||
- (NSURLRequest*) newQuitAppRequest;
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
||||
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
||||
|
||||
@@ -14,19 +14,23 @@
|
||||
#include <libxml2/libxml/xmlreader.h>
|
||||
#include <string.h>
|
||||
|
||||
#define SHORT_TIMEOUT_SEC 2
|
||||
#define NORMAL_TIMEOUT_SEC 5
|
||||
#define LONG_TIMEOUT_SEC 60
|
||||
#define EXTRA_LONG_TIMEOUT_SEC 180
|
||||
|
||||
@implementation HttpManager {
|
||||
NSURLSession* _urlSession;
|
||||
NSString* _baseHTTPURL;
|
||||
NSString* _baseHTTPSURL;
|
||||
NSString* _host;
|
||||
NSString* _uniqueId;
|
||||
NSString* _deviceName;
|
||||
NSData* _cert;
|
||||
NSData* _serverCert;
|
||||
NSMutableData* _respData;
|
||||
NSData* _requestResp;
|
||||
dispatch_semaphore_t _requestLock;
|
||||
|
||||
BOOL _errorOccurred;
|
||||
NSError* _error;
|
||||
}
|
||||
|
||||
static const NSString* HTTP_PORT = @"47989";
|
||||
@@ -39,49 +43,66 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
return [xmlString dataUsingEncoding:NSUTF8StringEncoding];
|
||||
}
|
||||
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert {
|
||||
- (void) setServerCert:(NSData*) serverCert {
|
||||
_serverCert = serverCert;
|
||||
}
|
||||
|
||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert {
|
||||
self = [super init];
|
||||
_host = host;
|
||||
_uniqueId = uniqueId;
|
||||
// Use the same UID for all Moonlight clients to allow them
|
||||
// quit games started on another Moonlight client.
|
||||
_uniqueId = @"0123456789ABCDEF";
|
||||
_deviceName = deviceName;
|
||||
_cert = cert;
|
||||
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", host, HTTP_PORT];
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", host, HTTPS_PORT];
|
||||
_serverCert = serverCert;
|
||||
_requestLock = dispatch_semaphore_create(0);
|
||||
_respData = [[NSMutableData alloc] init];
|
||||
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
|
||||
|
||||
// If this is an IPv6 literal, we must properly enclose it in brackets
|
||||
NSString* urlSafeHost;
|
||||
if ([host containsString:@":"]) {
|
||||
urlSafeHost = [NSString stringWithFormat:@"[%@]", host];
|
||||
} else {
|
||||
urlSafeHost = host;
|
||||
}
|
||||
|
||||
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", urlSafeHost, HTTP_PORT];
|
||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", urlSafeHost, HTTPS_PORT];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) executeRequestSynchronously:(HttpRequest*)request {
|
||||
Log(LOG_D, @"Making Request: %@", request);
|
||||
[_respData setLength:0];
|
||||
_error = nil;
|
||||
|
||||
Log(LOG_D, @"Making Request: %@", request);
|
||||
[[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
|
||||
|
||||
if (error != NULL) {
|
||||
Log(LOG_D, @"Connection error: %@", error);
|
||||
_errorOccurred = true;
|
||||
self->_error = error;
|
||||
}
|
||||
else {
|
||||
Log(LOG_D, @"Received response: %@", response);
|
||||
|
||||
if (data != NULL) {
|
||||
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
|
||||
[_respData appendData:data];
|
||||
if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) {
|
||||
_requestResp = [HttpManager fixXmlVersion:_respData];
|
||||
[self->_respData appendData:data];
|
||||
if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
|
||||
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
|
||||
} else {
|
||||
_requestResp = _respData;
|
||||
self->_requestResp = self->_respData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dispatch_semaphore_signal(_requestLock);
|
||||
dispatch_semaphore_signal(self->_requestLock);
|
||||
}] resume];
|
||||
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
|
||||
|
||||
if (!_errorOccurred && request.response) {
|
||||
if (!_error && request.response) {
|
||||
[request.response populateWithData:_requestResp];
|
||||
|
||||
// If the fallback error code was detected, issue the fallback request
|
||||
@@ -93,92 +114,127 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
[self executeRequestSynchronously:request];
|
||||
}
|
||||
}
|
||||
_errorOccurred = false;
|
||||
else if (_error && [_error code] == NSURLErrorServerCertificateUntrusted) {
|
||||
// We must have a pinned cert for HTTPS. If we fail, it must be due to
|
||||
// a non-matching cert, not because we had no cert at all.
|
||||
assert(_serverCert != nil);
|
||||
|
||||
if (request.fallbackRequest) {
|
||||
// This will fall back to HTTP on serverinfo queries to allow us to pair again
|
||||
// and get the server cert updated.
|
||||
Log(LOG_D, @"Attempting fallback request after certificate trust failure");
|
||||
request.request = request.fallbackRequest;
|
||||
request.fallbackError = 0;
|
||||
request.fallbackRequest = NULL;
|
||||
[self executeRequestSynchronously:request];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (NSURLRequest*) createRequestFromString:(NSString*) urlString enableTimeout:(BOOL)normalTimeout {
|
||||
- (NSURLRequest*) createRequestFromString:(NSString*) urlString timeout:(int)timeout {
|
||||
NSURL* url = [[NSURL alloc] initWithString:urlString];
|
||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||
if (normalTimeout) {
|
||||
// Timeout the request after 7 seconds
|
||||
[request setTimeoutInterval:7];
|
||||
}
|
||||
else {
|
||||
// Timeout the request after 60 seconds
|
||||
[request setTimeoutInterval:60];
|
||||
}
|
||||
[request setTimeoutInterval:timeout];
|
||||
return request;
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt {
|
||||
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=getservercert&salt=%@&clientcert=%@",
|
||||
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:_cert]];
|
||||
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:salt], [self bytesToHex:clientCert]];
|
||||
// This call blocks while waiting for the user to input the PIN on the PC
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
return [self createRequestFromString:urlString timeout:EXTRA_LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newUnpairRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
|
||||
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&serverchallengeresp=%@",
|
||||
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPSURL, _uniqueId, _deviceName, clientPairSecret];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPURL, _uniqueId, _deviceName, clientPairSecret];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newPairChallenge {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newAppListRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newServerInfoRequest {
|
||||
- (NSURLRequest *)newServerInfoRequest:(bool)fastFail {
|
||||
if (_serverCert == nil) {
|
||||
// Use HTTP if the cert is not pinned yet
|
||||
return [self newHttpServerInfoRequest:fastFail];
|
||||
}
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newHttpServerInfoRequest:(bool)fastFail {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
|
||||
return [self createRequestFromString:urlString timeout:(fastFail ? SHORT_TIMEOUT_SEC : NORMAL_TIMEOUT_SEC)];
|
||||
}
|
||||
|
||||
- (NSURLRequest *)newHttpServerInfoRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
|
||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
||||
return [self newHttpServerInfoRequest:false];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=1&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, appId, width, height, refreshRate, rikey, rikeyid];
|
||||
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
|
||||
// Using an FPS value over 60 causes SOPS to default to 720p60,
|
||||
// so force it to 60 when starting. This won't impact our ability
|
||||
// to get > 60 FPS while actually streaming though.
|
||||
int fps = config.frameRate > 60 ? 60 : config.frameRate;
|
||||
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/launch?uniqueid=%@&appid=%@&mode=%dx%dx%d&additionalStates=1&sops=%d&rikey=%@&rikeyid=%d%@&localAudioPlayMode=%d&surroundAudioInfo=%d&remoteControllersBitmap=%d&gcmap=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
config.appID,
|
||||
config.width, config.height, fps,
|
||||
config.optimizeGameSettings ? 1 : 0,
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
config.enableHdr ? @"&hdrMode=1&clientHdrCapVersion=0&clientHdrCapSupportedFlagsInUint32=0&clientHdrCapMetaDataId=NV_STATIC_METADATA_TYPE_1&clientHdrCapDisplayData=0x0x0x0x0x0x0x0x0x0x0": @"",
|
||||
config.playAudioOnPC ? 1 : 0,
|
||||
(config.audioChannelMask << 16) | config.audioChannelCount,
|
||||
config.gamepadMask, config.gamepadMask];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is launching
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, riKey, riKeyId];
|
||||
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d&surroundAudioInfo=%d",
|
||||
_baseHTTPSURL, _uniqueId,
|
||||
[Utils bytesToHex:config.riKey], config.riKeyId,
|
||||
(config.audioChannelMask << 16) | config.audioChannelCount];
|
||||
Log(LOG_I, @"Requesting: %@", urlString);
|
||||
// This blocks while the app is resuming
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newQuitAppRequest {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
|
||||
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, appId];
|
||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
||||
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||
}
|
||||
|
||||
- (NSString*) bytesToHex:(NSData*)data {
|
||||
@@ -229,6 +285,36 @@ static const NSString* HTTPS_PORT = @"47984";
|
||||
// Allow untrusted server certificates
|
||||
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
|
||||
{
|
||||
if (SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust) != 1) {
|
||||
Log(LOG_E, @"Server certificate count mismatch");
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
SecCertificateRef actualCert = SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, 0);
|
||||
if (actualCert == nil) {
|
||||
Log(LOG_E, @"Server certificate parsing error");
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
CFDataRef actualCertData = SecCertificateCopyData(actualCert);
|
||||
if (actualCertData == nil) {
|
||||
Log(LOG_E, @"Server certificate data parsing error");
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CFEqual(actualCertData, (__bridge CFDataRef)_serverCert)) {
|
||||
Log(LOG_E, @"Server certificate mismatch");
|
||||
CFRelease(actualCertData);
|
||||
completionHandler(NSURLSessionAuthChallengePerformDefaultHandling, NULL);
|
||||
return;
|
||||
}
|
||||
|
||||
CFRelease(actualCertData);
|
||||
|
||||
// Allow TLS handshake to proceed
|
||||
completionHandler(NSURLSessionAuthChallengeUseCredential,
|
||||
[NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "HttpResponse.h"
|
||||
|
||||
@interface HttpRequest : NSObject
|
||||
|
||||
@@ -6,8 +6,6 @@
|
||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
static NSString* TAG_STATUS_CODE = @"status_code";
|
||||
static NSString* TAG_STATUS_MESSAGE = @"status_message";
|
||||
|
||||
|
||||
@@ -89,6 +89,8 @@
|
||||
}
|
||||
|
||||
xmlFreeDoc(docPtr);
|
||||
|
||||
Log(LOG_D, @"Parsed XML data: %@", _elements);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||
//
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
@protocol MDNSCallback <NSObject>
|
||||
|
||||
- (void) updateHosts:(NSArray*)hosts;
|
||||
- (void) updateHost:(TemporaryHost*)host;
|
||||
|
||||
@end
|
||||
|
||||
|
||||
@@ -9,11 +9,15 @@
|
||||
#import "MDNSManager.h"
|
||||
#import "TemporaryHost.h"
|
||||
|
||||
#include <arpa/inet.h>
|
||||
|
||||
#include <Limelight.h>
|
||||
|
||||
@implementation MDNSManager {
|
||||
NSNetServiceBrowser* mDNSBrowser;
|
||||
NSMutableArray* domains;
|
||||
NSMutableArray* services;
|
||||
BOOL scanActive;
|
||||
BOOL timerPending;
|
||||
}
|
||||
|
||||
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
@@ -23,43 +27,197 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
|
||||
self.callback = callback;
|
||||
|
||||
scanActive = FALSE;
|
||||
|
||||
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
|
||||
[mDNSBrowser setDelegate:self];
|
||||
|
||||
domains = [[NSMutableArray alloc] init];
|
||||
services = [[NSMutableArray alloc] init];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void) searchForHosts {
|
||||
if (scanActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Starting mDNS discovery");
|
||||
scanActive = TRUE;
|
||||
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
|
||||
|
||||
if (!timerPending) {
|
||||
timerPending = TRUE;
|
||||
|
||||
// Just invoke the timer callback to save a little code
|
||||
[self startSearchTimerCallback:nil];
|
||||
}
|
||||
}
|
||||
|
||||
- (void) stopSearching {
|
||||
if (!scanActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Stopping mDNS discovery");
|
||||
scanActive = FALSE;
|
||||
[mDNSBrowser stop];
|
||||
}
|
||||
|
||||
- (NSArray*) getFoundHosts {
|
||||
NSMutableArray* hosts = [[NSMutableArray alloc] init];
|
||||
for (NSNetService* service in services) {
|
||||
if (service.hostName != nil) {
|
||||
TemporaryHost* host = [[TemporaryHost alloc] init];
|
||||
host.activeAddress = host.localAddress = service.hostName;
|
||||
host.name = service.hostName;
|
||||
[hosts addObject:host];
|
||||
+ (NSString*)sockAddrToString:(NSData*)addrData {
|
||||
char addrStr[INET6_ADDRSTRLEN];
|
||||
struct sockaddr* addr = (struct sockaddr*)[addrData bytes];
|
||||
if (addr->sa_family == AF_INET) {
|
||||
inet_ntop(addr->sa_family, &((struct sockaddr_in*)addr)->sin_addr, addrStr, sizeof(addrStr));
|
||||
}
|
||||
else {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)addr;
|
||||
inet_ntop(addr->sa_family, &sin6->sin6_addr, addrStr, sizeof(addrStr));
|
||||
if (sin6->sin6_scope_id != 0) {
|
||||
// Link-local addresses with scope IDs are special
|
||||
return [NSString stringWithFormat: @"%s%%%u", addrStr, sin6->sin6_scope_id];
|
||||
}
|
||||
}
|
||||
return hosts;
|
||||
return [NSString stringWithFormat: @"%s", addrStr];
|
||||
}
|
||||
|
||||
+ (BOOL)isAddress:(uint8_t*)address inSubnet:(uint8_t*)subnet netmask:(int)bits {
|
||||
for (int i = 0; i < bits; i++) {
|
||||
uint8_t mask = 1 << (i % 8);
|
||||
if ((address[i / 8] & mask) != (subnet[i / 8] & mask)) {
|
||||
return NO;
|
||||
}
|
||||
}
|
||||
return YES;
|
||||
}
|
||||
|
||||
+ (BOOL)isLocalIpv6Address:(NSData*)addrData {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)[addrData bytes];
|
||||
if (sin6->sin6_family != AF_INET6) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
uint8_t* addrBytes = sin6->sin6_addr.s6_addr;
|
||||
uint8_t prefix[2];
|
||||
|
||||
// fe80::/10
|
||||
prefix[0] = 0xfe;
|
||||
prefix[1] = 0x80;
|
||||
if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:10]) {
|
||||
// Link-local
|
||||
return YES;
|
||||
}
|
||||
|
||||
// fec0::/10
|
||||
prefix[0] = 0xfe;
|
||||
prefix[1] = 0xc0;
|
||||
if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:10]) {
|
||||
// Site local
|
||||
return YES;
|
||||
}
|
||||
|
||||
// fc00::/7
|
||||
prefix[0] = 0xfc;
|
||||
prefix[1] = 0x00;
|
||||
if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:7]) {
|
||||
// ULA
|
||||
return YES;
|
||||
}
|
||||
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (NSString*)getBestIpv6Address:(NSArray<NSData*>*)addresses {
|
||||
for (NSData* addrData in addresses) {
|
||||
struct sockaddr_in6* sin6 = (struct sockaddr_in6*)[addrData bytes];
|
||||
if (sin6->sin6_family != AF_INET6) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ([MDNSManager isLocalIpv6Address:addrData]) {
|
||||
// Skip non-global addresses
|
||||
continue;
|
||||
}
|
||||
|
||||
uint8_t* addrBytes = sin6->sin6_addr.s6_addr;
|
||||
uint8_t prefix[2];
|
||||
|
||||
// 2002::/16
|
||||
prefix[0] = 0x20;
|
||||
prefix[1] = 0x02;
|
||||
if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:16]) {
|
||||
Log(LOG_I, @"Ignoring 6to4 address: %@", [MDNSManager sockAddrToString:addrData]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2001::/32
|
||||
prefix[0] = 0x20;
|
||||
prefix[1] = 0x01;
|
||||
if ([MDNSManager isAddress:addrBytes inSubnet:prefix netmask:32]) {
|
||||
Log(LOG_I, @"Ignoring Teredo address: %@", [MDNSManager sockAddrToString:addrData]);
|
||||
continue;
|
||||
}
|
||||
|
||||
return [MDNSManager sockAddrToString:addrData];
|
||||
}
|
||||
|
||||
return nil;
|
||||
}
|
||||
|
||||
- (void)netServiceDidResolveAddress:(NSNetService *)service {
|
||||
Log(LOG_I, @"Resolved address: %@ -> %@", service, service.hostName);
|
||||
[self.callback updateHosts:[self getFoundHosts]];
|
||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||
NSArray<NSData*>* addresses = [service addresses];
|
||||
|
||||
for (NSData* addrData in addresses) {
|
||||
Log(LOG_I, @"Resolved address: %@ -> %@", [service hostName], [MDNSManager sockAddrToString: addrData]);
|
||||
}
|
||||
|
||||
TemporaryHost* host = [[TemporaryHost alloc] init];
|
||||
|
||||
// First, look for an IPv4 record for the local address
|
||||
for (NSData* addrData in addresses) {
|
||||
struct sockaddr_in* sin = (struct sockaddr_in*)[addrData bytes];
|
||||
if (sin->sin_family != AF_INET) {
|
||||
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);
|
||||
}
|
||||
|
||||
host.localAddress = [MDNSManager sockAddrToString:addrData];
|
||||
Log(LOG_I, @"Local address chosen: %@ -> %@", [service hostName], host.localAddress);
|
||||
break;
|
||||
}
|
||||
|
||||
if (host.localAddress == nil) {
|
||||
// If we didn't find an IPv4 record, look for a local IPv6 record
|
||||
for (NSData* addrData in addresses) {
|
||||
if ([MDNSManager isLocalIpv6Address:addrData]) {
|
||||
host.localAddress = [MDNSManager sockAddrToString:addrData];
|
||||
Log(LOG_I, @"Local address chosen: %@ -> %@", [service hostName], host.localAddress);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
host.ipv6Address = [MDNSManager getBestIpv6Address:addresses];
|
||||
Log(LOG_I, @"IPv6 address chosen: %@ -> %@", [service hostName], host.ipv6Address);
|
||||
|
||||
host.activeAddress = host.localAddress;
|
||||
host.name = service.hostName;
|
||||
[self.callback updateHost:host];
|
||||
});
|
||||
}
|
||||
|
||||
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
|
||||
@@ -74,12 +232,14 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
}
|
||||
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||
Log(LOG_I, @"Found service: %@", aNetService);
|
||||
[aNetService setDelegate:self];
|
||||
[aNetService resolveWithTimeout:5];
|
||||
Log(LOG_D, @"Found service: %@", aNetService);
|
||||
|
||||
[services removeObject:aNetService];
|
||||
[services addObject:aNetService];
|
||||
if (![services containsObject:aNetService]) {
|
||||
Log(LOG_I, @"Found new host: %@", aNetService.name);
|
||||
[aNetService setDelegate:self];
|
||||
[aNetService resolveWithTimeout:5];
|
||||
[services addObject:aNetService];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||
@@ -90,23 +250,29 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
|
||||
Log(LOG_W, @"Did not perform search: \n%@", [errorDict description]);
|
||||
|
||||
// Schedule a retry in 2 seconds
|
||||
[NSTimer scheduledTimerWithTimeInterval:2.0
|
||||
target:self
|
||||
selector:@selector(retrySearchTimerCallback:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
// We'll schedule a retry in startSearchTimerCallback
|
||||
}
|
||||
|
||||
- (void)retrySearchTimerCallback:(NSTimer *)timer {
|
||||
- (void)startSearchTimerCallback:(NSTimer *)timer {
|
||||
// Check if we've been stopped since this was queued
|
||||
if (!scanActive) {
|
||||
timerPending = FALSE;
|
||||
return;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"Retrying mDNS search");
|
||||
Log(LOG_D, @"Restarting mDNS search");
|
||||
[mDNSBrowser stop];
|
||||
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
|
||||
|
||||
// Search again in 5 seconds. We need to do this because
|
||||
// we want more aggressive querying than Bonjour will normally
|
||||
// do for when we're at the hosts screen. This also covers scenarios
|
||||
// where discovery didn't work, like if WiFi was disabled.
|
||||
[NSTimer scheduledTimerWithTimeInterval:5.0
|
||||
target:self
|
||||
selector:@selector(startSearchTimerCallback:)
|
||||
userInfo:nil
|
||||
repeats:NO];
|
||||
}
|
||||
|
||||
- (void)retryResolveTimerCallback:(NSTimer *)timer {
|
||||
@@ -124,4 +290,4 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@end
|
||||
|
||||