mirror of
https://github.com/moonlight-stream/moonlight-qt.git
synced 2026-10-22 08:07:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
No files matched your search
@@ -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
|
||||||
@@ -6,11 +6,19 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
|
|
||||||
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
@interface AppDelegate : UIResponder <UIApplicationDelegate>
|
||||||
|
|
||||||
@property (strong, nonatomic) UIWindow *window;
|
@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) NSManagedObjectContext *managedObjectContext;
|
||||||
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
@property (readonly, strong, nonatomic) NSManagedObjectModel *managedObjectModel;
|
||||||
|
|||||||
+93
-37
@@ -16,35 +16,17 @@
|
|||||||
|
|
||||||
static NSOperationQueue* mainQueue;
|
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
|
- (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;
|
return YES;
|
||||||
}
|
}
|
||||||
@@ -76,15 +58,36 @@ static NSOperationQueue* mainQueue;
|
|||||||
// Saves changes in the application's managed object context before the application terminates.
|
// Saves changes in the application's managed object context before the application terminates.
|
||||||
[self saveContext];
|
[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
|
- (void)saveContext
|
||||||
{
|
{
|
||||||
NSError *error = nil;
|
NSManagedObjectContext *managedObjectContext = [self managedObjectContext];
|
||||||
NSManagedObjectContext *managedObjectContext = self.managedObjectContext;
|
|
||||||
if (managedObjectContext != nil) {
|
if (managedObjectContext != nil) {
|
||||||
if ([managedObjectContext hasChanges] && ![managedObjectContext save:&error]) {
|
[managedObjectContext performBlock:^{
|
||||||
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
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;
|
return _persistentStoreCoordinator;
|
||||||
}
|
}
|
||||||
|
|
||||||
NSURL *storeURL = [self getStoreURL];
|
|
||||||
|
|
||||||
NSError *error = nil;
|
NSError *error = nil;
|
||||||
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
_persistentStoreCoordinator = [[NSPersistentStoreCoordinator alloc] initWithManagedObjectModel:[self managedObjectModel]];
|
||||||
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
|
NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
|
||||||
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
|
[NSNumber numberWithBool:YES], NSMigratePersistentStoresAutomaticallyOption,
|
||||||
[NSNumber numberWithBool:YES], NSInferMappingModelAutomaticallyOption, nil];
|
[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 the error
|
||||||
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
Log(LOG_E, @"Critical database error: %@, %@", error, [error userInfo]);
|
||||||
|
|
||||||
// Drop the database
|
// Drop the database
|
||||||
[[NSFileManager defaultManager] removeItemAtURL:storeURL error:nil];
|
[self dropDatabase];
|
||||||
|
|
||||||
// Try again
|
// Try again
|
||||||
return [self persistentStoreCoordinator];
|
return [self persistentStoreCoordinator];
|
||||||
@@ -154,8 +168,50 @@ static NSOperationQueue* mainQueue;
|
|||||||
return [[[NSFileManager defaultManager] URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject];
|
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 {
|
- (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
|
@end
|
||||||
@@ -6,16 +6,14 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@interface CryptoManager : NSObject
|
@interface CryptoManager : NSObject
|
||||||
|
|
||||||
+ (void) generateKeyPairUsingSSl;
|
+ (void) generateKeyPairUsingSSL;
|
||||||
+ (NSData*) readCertFromFile;
|
+ (NSData*) readCertFromFile;
|
||||||
+ (NSData*) readKeyFromFile;
|
+ (NSData*) readKeyFromFile;
|
||||||
+ (NSData*) readP12FromFile;
|
+ (NSData*) readP12FromFile;
|
||||||
+ (NSData*) getSignatureFromCert:(NSData*)cert;
|
+ (NSData*) getSignatureFromCert:(NSData*)cert;
|
||||||
+ (NSData*) nullTerminateString:(NSData*)data;
|
+ (NSData*) pemToDer:(NSData*)pemCertBytes;
|
||||||
|
|
||||||
- (NSData*) createAESKeyFromSaltSHA1:(NSData*)saltedPIN;
|
- (NSData*) createAESKeyFromSaltSHA1:(NSData*)saltedPIN;
|
||||||
- (NSData*) createAESKeyFromSaltSHA256:(NSData*)saltedPIN;
|
- (NSData*) createAESKeyFromSaltSHA256:(NSData*)saltedPIN;
|
||||||
|
|||||||
@@ -87,17 +87,28 @@ static NSData* p12 = nil;
|
|||||||
return (((int)[data length] + 15) / 16) * 16;
|
return (((int)[data length] + 15) / 16) * 16;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSData*) nullTerminateString:(NSData*)data {
|
+ (NSData*) pemToDer:(NSData*)pemCertBytes {
|
||||||
NSMutableData* mutData = [NSMutableData dataWithData:data];
|
X509* x509;
|
||||||
[mutData appendBytes:"" length:1];
|
|
||||||
return mutData;
|
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 {
|
- (bool) verifySignature:(NSData *)data withSignature:(NSData*)signature andCert:(NSData*)cert {
|
||||||
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
|
|
||||||
X509* x509;
|
X509* x509;
|
||||||
BIO* bio = BIO_new(BIO_s_mem());
|
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||||
BIO_puts(bio, buffer);
|
|
||||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||||
|
|
||||||
BIO_free(bio);
|
BIO_free(bio);
|
||||||
@@ -122,9 +133,7 @@ static NSData* p12 = nil;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
|
- (NSData *)signData:(NSData *)data withKey:(NSData *)key {
|
||||||
const char* buffer = [[CryptoManager nullTerminateString:key] bytes];
|
BIO* bio = BIO_new_mem_buf([key bytes], (int)[key length]);
|
||||||
BIO* bio = BIO_new(BIO_s_mem());
|
|
||||||
BIO_puts(bio, buffer);
|
|
||||||
|
|
||||||
EVP_PKEY* pkey;
|
EVP_PKEY* pkey;
|
||||||
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
pkey = PEM_read_bio_PrivateKey(bio, NULL, NULL, NULL);
|
||||||
@@ -159,57 +168,60 @@ static NSData* p12 = nil;
|
|||||||
return signedData;
|
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 {
|
+ (NSData*) readCertFromFile {
|
||||||
if (cert == nil) {
|
if (cert == nil) {
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
cert = [CryptoManager readCryptoObject:@"client.crt"];
|
||||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
|
||||||
NSString *certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
|
|
||||||
cert = [NSData dataWithContentsOfFile:certFile];
|
|
||||||
}
|
}
|
||||||
return cert;
|
return cert;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSData*) readP12FromFile {
|
+ (NSData*) readP12FromFile {
|
||||||
if (p12 == nil) {
|
if (p12 == nil) {
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
p12 = [CryptoManager readCryptoObject:@"client.p12"];
|
||||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
|
||||||
NSString *p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
|
|
||||||
p12 = [NSData dataWithContentsOfFile:p12File];
|
|
||||||
}
|
}
|
||||||
return p12;
|
return p12;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSData*) readKeyFromFile {
|
+ (NSData*) readKeyFromFile {
|
||||||
if (key == nil) {
|
if (key == nil) {
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
key = [CryptoManager readCryptoObject:@"client.key"];
|
||||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
|
||||||
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
|
||||||
key = [NSData dataWithContentsOfFile:keyFile];
|
|
||||||
}
|
}
|
||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (bool) keyPairExists {
|
+ (bool) keyPairExists {
|
||||||
NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
bool keyFileExists = [CryptoManager readCryptoObject:@"client.key"] != nil;
|
||||||
NSString *documentsDirectory = [paths objectAtIndex:0];
|
bool p12FileExists = [CryptoManager readCryptoObject:@"client.p12"] != nil;
|
||||||
NSString *keyFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
bool certFileExists = [CryptoManager readCryptoObject:@"client.crt"] != nil;
|
||||||
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];
|
|
||||||
|
|
||||||
return keyFileExists && p12FileExists && certFileExists;
|
return keyFileExists && p12FileExists && certFileExists;
|
||||||
}
|
}
|
||||||
|
|
||||||
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
+ (NSData *)getSignatureFromCert:(NSData *)cert {
|
||||||
const char* buffer = [[CryptoManager nullTerminateString:cert] bytes];
|
BIO* bio = BIO_new_mem_buf([cert bytes], (int)[cert length]);
|
||||||
X509* x509;
|
X509* x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
||||||
BIO* bio = BIO_new(BIO_s_mem());
|
|
||||||
BIO_puts(bio, buffer);
|
|
||||||
x509 = PEM_read_bio_X509(bio, NULL, NULL, NULL);
|
|
||||||
|
|
||||||
if (!x509) {
|
if (!x509) {
|
||||||
Log(LOG_E, @"Unable to parse certificate in memory!");
|
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];
|
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;
|
static dispatch_once_t pred;
|
||||||
dispatch_once(&pred, ^{
|
dispatch_once(&pred, ^{
|
||||||
if (![CryptoManager keyPairExists]) {
|
if (![CryptoManager keyPairExists]) {
|
||||||
|
|
||||||
Log(LOG_I, @"Generating Certificate... ");
|
Log(LOG_I, @"Generating Certificate... ");
|
||||||
CertKeyPair certKeyPair = generateCertKeyPair();
|
CertKeyPair certKeyPair = generateCertKeyPair();
|
||||||
|
|
||||||
NSArray* paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
|
NSData* certData = [CryptoManager getCertFromCertKeyPair:&certKeyPair];
|
||||||
NSString* documentsDirectory = [paths objectAtIndex:0];
|
NSData* p12Data = [CryptoManager getP12FromCertKeyPair:&certKeyPair];
|
||||||
NSString* certFile = [documentsDirectory stringByAppendingPathComponent:@"client.crt"];
|
NSData* keyData = [CryptoManager getKeyFromCertKeyPair:&certKeyPair];
|
||||||
NSString* keyPairFile = [documentsDirectory stringByAppendingPathComponent:@"client.key"];
|
|
||||||
NSString* p12File = [documentsDirectory stringByAppendingPathComponent:@"client.p12"];
|
|
||||||
|
|
||||||
//Log(LOG_D, @"Writing cert and key to: \n%@\n%@", certFile, keyPairFile);
|
|
||||||
saveCertKeyPair([certFile UTF8String], [p12File UTF8String], [keyPairFile UTF8String], certKeyPair);
|
|
||||||
freeCertKeyPair(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");
|
Log(LOG_I, @"Certificate created");
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@interface IdManager : NSObject
|
@interface IdManager : NSObject
|
||||||
|
|
||||||
+ (NSString*) getUniqueId;
|
+ (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 mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years);
|
||||||
int add_ext(X509 *cert, int nid, char *value);
|
int add_ext(X509 *cert, int nid, char *value);
|
||||||
|
|
||||||
struct CertKeyPair generateCertKeyPair() {
|
struct CertKeyPair generateCertKeyPair(void) {
|
||||||
BIO *bio_err;
|
BIO *bio_err;
|
||||||
X509 *x509 = NULL;
|
X509 *x509 = NULL;
|
||||||
EVP_PKEY *pkey = NULL;
|
EVP_PKEY *pkey = NULL;
|
||||||
@@ -62,21 +62,6 @@ void freeCertKeyPair(struct CertKeyPair certKeyPair) {
|
|||||||
PKCS12_free(certKeyPair.p12);
|
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) {
|
int mkcert(X509 **x509p, EVP_PKEY **pkeyp, int bits, int serial, int years) {
|
||||||
X509 *x;
|
X509 *x;
|
||||||
EVP_PKEY *pk;
|
EVP_PKEY *pk;
|
||||||
|
|||||||
@@ -18,8 +18,7 @@ typedef struct CertKeyPair {
|
|||||||
PKCS12 *p12;
|
PKCS12 *p12;
|
||||||
} CertKeyPair;
|
} CertKeyPair;
|
||||||
|
|
||||||
struct CertKeyPair generateCertKeyPair();
|
struct CertKeyPair generateCertKeyPair(void);
|
||||||
void freeCertKeyPair(CertKeyPair);
|
void freeCertKeyPair(CertKeyPair);
|
||||||
void saveCertKeyPair(const char* certFile, const char* p12File, const char* keyPairFile, CertKeyPair certKeyPair);
|
|
||||||
#endif
|
#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.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "AppDelegate.h"
|
#import "AppDelegate.h"
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
@@ -14,12 +13,21 @@
|
|||||||
|
|
||||||
@interface DataManager : NSObject
|
@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;
|
- (NSArray*) getHosts;
|
||||||
- (void) updateHost:(TemporaryHost*)host;
|
- (void) updateHost:(TemporaryHost*)host;
|
||||||
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
- (void) updateAppsForExistingHost:(TemporaryHost *)host;
|
||||||
- (void) updateIconForExistingApp:(TemporaryApp*)app;
|
|
||||||
- (void) removeHost:(TemporaryHost*)host;
|
- (void) removeHost:(TemporaryHost*)host;
|
||||||
- (void) removeApp:(TemporaryApp*)app;
|
- (void) removeApp:(TemporaryApp*)app;
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
#import "TemporarySettings.h"
|
#import "TemporarySettings.h"
|
||||||
#import "Settings.h"
|
|
||||||
|
|
||||||
@implementation DataManager {
|
@implementation DataManager {
|
||||||
NSManagedObjectContext *_managedObjectContext;
|
NSManagedObjectContext *_managedObjectContext;
|
||||||
@@ -22,16 +21,17 @@
|
|||||||
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
|
// HACK: Avoid calling [UIApplication delegate] off the UI thread to keep
|
||||||
// Main Thread Checker happy.
|
// Main Thread Checker happy.
|
||||||
if ([NSThread isMainThread]) {
|
if ([NSThread isMainThread]) {
|
||||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||||
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
dispatch_sync(dispatch_get_main_queue(), ^{
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
_appDelegate = (AppDelegate *)[[UIApplication sharedApplication] delegate];
|
self->_appDelegate = (AppDelegate *)[[OSApplication sharedApplication] delegate];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
_managedObjectContext = [[NSManagedObjectContext alloc] initWithConcurrencyType:NSMainQueueConcurrencyType];
|
||||||
[_managedObjectContext setPersistentStoreCoordinator:_appDelegate.persistentStoreCoordinator];
|
[_managedObjectContext setParentContext:[_appDelegate managedObjectContext]];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
@@ -53,16 +53,31 @@
|
|||||||
return uid;
|
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:^{
|
[_managedObjectContext performBlockAndWait:^{
|
||||||
Settings* settingsToSave = [self retrieveSettings];
|
Settings* settingsToSave = [self retrieveSettings];
|
||||||
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
settingsToSave.framerate = [NSNumber numberWithInteger:framerate];
|
||||||
// Bitrate is persisted in kbps
|
|
||||||
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
settingsToSave.bitrate = [NSNumber numberWithInteger:bitrate];
|
||||||
settingsToSave.height = [NSNumber numberWithInteger:height];
|
settingsToSave.height = [NSNumber numberWithInteger:height];
|
||||||
settingsToSave.width = [NSNumber numberWithInteger:width];
|
settingsToSave.width = [NSNumber numberWithInteger:width];
|
||||||
settingsToSave.onscreenControls = [NSNumber numberWithInteger:onscreenControls];
|
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];
|
[self saveData];
|
||||||
}];
|
}];
|
||||||
@@ -73,8 +88,8 @@
|
|||||||
// Add a new persistent managed object if one doesn't exist
|
// Add a new persistent managed object if one doesn't exist
|
||||||
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
Host* parent = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
||||||
if (parent == nil) {
|
if (parent == nil) {
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:_managedObjectContext];
|
NSEntityDescription* entity = [NSEntityDescription entityForName:@"Host" inManagedObjectContext:self->_managedObjectContext];
|
||||||
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
|
parent = [[Host alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Push changes from the temp host to the persistent one
|
// 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
|
// Add a new persistent managed object if one doesn't exist
|
||||||
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
|
App* parentApp = [self getAppForTemporaryApp:app withAppRecords:appRecords];
|
||||||
if (parentApp == nil) {
|
if (parentApp == nil) {
|
||||||
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:_managedObjectContext];
|
NSEntityDescription* entity = [NSEntityDescription entityForName:@"App" inManagedObjectContext:self->_managedObjectContext];
|
||||||
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:_managedObjectContext];
|
parentApp = [[App alloc] initWithEntity:entity insertIntoManagedObjectContext:self->_managedObjectContext];
|
||||||
}
|
}
|
||||||
|
|
||||||
[app propagateChangesToParent:parentApp withHost:parent];
|
[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 {
|
- (TemporarySettings*) getSettings {
|
||||||
__block TemporarySettings *tempSettings;
|
__block TemporarySettings *tempSettings;
|
||||||
|
|
||||||
@@ -155,7 +156,7 @@
|
|||||||
[_managedObjectContext performBlockAndWait:^{
|
[_managedObjectContext performBlockAndWait:^{
|
||||||
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
|
App* managedApp = [self getAppForTemporaryApp:app withAppRecords:[self fetchRecords:@"App"]];
|
||||||
if (managedApp != nil) {
|
if (managedApp != nil) {
|
||||||
[_managedObjectContext deleteObject:managedApp];
|
[self->_managedObjectContext deleteObject:managedApp];
|
||||||
[self saveData];
|
[self saveData];
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
@@ -165,7 +166,7 @@
|
|||||||
[_managedObjectContext performBlockAndWait:^{
|
[_managedObjectContext performBlockAndWait:^{
|
||||||
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
Host* managedHost = [self getHostForTemporaryHost:host withHostRecords:[self fetchRecords:@"Host"]];
|
||||||
if (managedHost != nil) {
|
if (managedHost != nil) {
|
||||||
[_managedObjectContext deleteObject:managedHost];
|
[self->_managedObjectContext deleteObject:managedHost];
|
||||||
[self saveData];
|
[self saveData];
|
||||||
}
|
}
|
||||||
}];
|
}];
|
||||||
@@ -173,7 +174,7 @@
|
|||||||
|
|
||||||
- (void) saveData {
|
- (void) saveData {
|
||||||
NSError* error;
|
NSError* error;
|
||||||
if (![_managedObjectContext save:&error]) {
|
if ([_managedObjectContext hasChanges] && ![_managedObjectContext save:&error]) {
|
||||||
Log(LOG_E, @"Unable to save hosts to database: %@", error);
|
Log(LOG_E, @"Unable to save hosts to database: %@", error);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,7 +210,7 @@
|
|||||||
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
|
- (App*) getAppForTemporaryApp:(TemporaryApp*)tempApp withAppRecords:(NSArray*)apps {
|
||||||
for (App* app in apps) {
|
for (App* app in apps) {
|
||||||
if ([app.id isEqualToString:tempApp.id] &&
|
if ([app.id isEqualToString:tempApp.id] &&
|
||||||
[app.host.uuid isEqualToString:app.host.uuid]) {
|
[app.host.uuid isEqualToString:tempApp.host.uuid]) {
|
||||||
return app;
|
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,14 @@
|
|||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
#import "App.h"
|
#import "App+CoreDataClass.h"
|
||||||
|
|
||||||
@interface TemporaryApp : NSObject
|
@interface TemporaryApp : NSObject
|
||||||
|
|
||||||
@property (nullable, nonatomic, retain) NSString *id;
|
@property (nullable, nonatomic, retain) NSString *id;
|
||||||
@property (nullable, nonatomic, retain) NSData *image;
|
|
||||||
@property (nullable, nonatomic, retain) NSString *name;
|
@property (nullable, nonatomic, retain) NSString *name;
|
||||||
|
@property (nonatomic) BOOL hdrSupported;
|
||||||
@property (nullable, nonatomic, retain) TemporaryHost *host;
|
@property (nullable, nonatomic, retain) TemporaryHost *host;
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
NS_ASSUME_NONNULL_BEGIN
|
||||||
|
|||||||
@@ -7,7 +7,6 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
#import "App.h"
|
|
||||||
|
|
||||||
@implementation TemporaryApp
|
@implementation TemporaryApp
|
||||||
|
|
||||||
@@ -15,8 +14,8 @@
|
|||||||
self = [self init];
|
self = [self init];
|
||||||
|
|
||||||
self.id = app.id;
|
self.id = app.id;
|
||||||
self.image = app.image;
|
|
||||||
self.name = app.name;
|
self.name = app.name;
|
||||||
|
self.hdrSupported = app.hdrSupported;
|
||||||
self.host = tempHost;
|
self.host = tempHost;
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
- (void) propagateChangesToParent:(App*)parent withHost:(Host*)host {
|
- (void) propagateChangesToParent:(App*)parent withHost:(Host*)host {
|
||||||
parent.id = self.id;
|
parent.id = self.id;
|
||||||
parent.name = self.name;
|
parent.name = self.name;
|
||||||
|
parent.hdrSupported = self.hdrSupported;
|
||||||
parent.host = host;
|
parent.host = host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,8 @@
|
|||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
#import "Host.h"
|
#import "Host+CoreDataClass.h"
|
||||||
|
|
||||||
@interface TemporaryHost : NSObject
|
@interface TemporaryHost : NSObject
|
||||||
|
|
||||||
@@ -17,12 +16,15 @@
|
|||||||
@property (nonatomic, nullable) NSString * activeAddress;
|
@property (nonatomic, nullable) NSString * activeAddress;
|
||||||
@property (nonatomic, nullable) NSString * currentGame;
|
@property (nonatomic, nullable) NSString * currentGame;
|
||||||
|
|
||||||
|
@property (nonatomic, nullable, retain) NSData *serverCert;
|
||||||
|
@property (nonatomic, nullable, retain) NSString *address;
|
||||||
|
@property (nonatomic, nullable, retain) NSString *externalAddress;
|
||||||
|
@property (nonatomic, nullable, retain) NSString *localAddress;
|
||||||
|
@property (nonatomic, nullable, retain) NSString *mac;
|
||||||
|
@property (nonatomic) int serverCodecModeSupport;
|
||||||
|
|
||||||
NS_ASSUME_NONNULL_BEGIN
|
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 *name;
|
||||||
@property (nonatomic, retain) NSString *uuid;
|
@property (nonatomic, retain) NSString *uuid;
|
||||||
@property (nonatomic, retain) NSMutableSet *appList;
|
@property (nonatomic, retain) NSMutableSet *appList;
|
||||||
|
|||||||
@@ -8,9 +8,7 @@
|
|||||||
|
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
#import "Host.h"
|
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
#import "App.h"
|
|
||||||
|
|
||||||
@implementation TemporaryHost
|
@implementation TemporaryHost
|
||||||
|
|
||||||
@@ -31,7 +29,11 @@
|
|||||||
self.mac = host.mac;
|
self.mac = host.mac;
|
||||||
self.name = host.name;
|
self.name = host.name;
|
||||||
self.uuid = host.uuid;
|
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];
|
NSMutableSet *appList = [[NSMutableSet alloc] init];
|
||||||
|
|
||||||
@@ -46,12 +48,27 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void) propagateChangesToParent:(Host*)parentHost {
|
- (void) propagateChangesToParent:(Host*)parentHost {
|
||||||
parentHost.address = self.address;
|
// Avoid overwriting existing data with nil if
|
||||||
parentHost.externalAddress = self.externalAddress;
|
// we don't have everything populated in the temporary
|
||||||
parentHost.localAddress = self.localAddress;
|
// host.
|
||||||
parentHost.mac = self.mac;
|
if (self.address != nil) {
|
||||||
|
parentHost.address = self.address;
|
||||||
|
}
|
||||||
|
if (self.externalAddress != nil) {
|
||||||
|
parentHost.externalAddress = self.externalAddress;
|
||||||
|
}
|
||||||
|
if (self.localAddress != nil) {
|
||||||
|
parentHost.localAddress = self.localAddress;
|
||||||
|
}
|
||||||
|
if (self.mac != nil) {
|
||||||
|
parentHost.mac = self.mac;
|
||||||
|
}
|
||||||
|
if (self.serverCert != nil) {
|
||||||
|
parentHost.serverCert = self.serverCert;
|
||||||
|
}
|
||||||
parentHost.name = self.name;
|
parentHost.name = self.name;
|
||||||
parentHost.uuid = self.uuid;
|
parentHost.uuid = self.uuid;
|
||||||
|
parentHost.serverCodecModeSupport = self.serverCodecModeSupport;
|
||||||
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
|
parentHost.pairState = [NSNumber numberWithInt:self.pairState];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@
|
|||||||
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
// Copyright © 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import "Settings+CoreDataClass.h"
|
||||||
#import "Settings.h"
|
|
||||||
|
|
||||||
@interface TemporarySettings : NSObject
|
@interface TemporarySettings : NSObject
|
||||||
|
|
||||||
@@ -19,6 +18,12 @@
|
|||||||
@property (nonatomic, retain) NSNumber * width;
|
@property (nonatomic, retain) NSNumber * width;
|
||||||
@property (nonatomic, retain) NSNumber * onscreenControls;
|
@property (nonatomic, retain) NSNumber * onscreenControls;
|
||||||
@property (nonatomic, retain) NSString * uniqueId;
|
@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;
|
- (id) initFromSettings:(Settings*)settings;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "TemporarySettings.h"
|
#import "TemporarySettings.h"
|
||||||
|
#import "OnScreenControls.h"
|
||||||
|
|
||||||
@implementation TemporarySettings
|
@implementation TemporarySettings
|
||||||
|
|
||||||
@@ -15,12 +16,59 @@
|
|||||||
|
|
||||||
self.parent = settings;
|
self.parent = settings;
|
||||||
|
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
NSInteger _bitrate = [[NSUserDefaults standardUserDefaults] integerForKey:@"bitrate"];
|
||||||
|
NSInteger _framerate = [[NSUserDefaults standardUserDefaults] integerForKey:@"framerate"];
|
||||||
|
|
||||||
|
if (_bitrate) {
|
||||||
|
self.bitrate = [NSNumber numberWithInteger:_bitrate];
|
||||||
|
} else {
|
||||||
|
self.bitrate = [NSNumber numberWithInteger:20000];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_framerate) {
|
||||||
|
self.framerate = [NSNumber numberWithInteger:_framerate];
|
||||||
|
} else {
|
||||||
|
self.framerate = [NSNumber numberWithInteger:60];
|
||||||
|
}
|
||||||
|
|
||||||
|
self.useHevc = [[NSUserDefaults standardUserDefaults] boolForKey:@"useHevc"] || NO;
|
||||||
|
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"] || NO;
|
||||||
|
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"] || NO;
|
||||||
|
self.optimizeGames = [[NSUserDefaults standardUserDefaults] boolForKey:@"optimizeGames"] || YES;
|
||||||
|
self.multiController = [[NSUserDefaults standardUserDefaults] boolForKey:@"multipleControllers"] || YES;
|
||||||
|
|
||||||
|
NSInteger _screenSize = [[NSUserDefaults standardUserDefaults] integerForKey:@"streamResolution"];
|
||||||
|
switch (_screenSize) {
|
||||||
|
case 0:
|
||||||
|
self.height = [NSNumber numberWithInteger:720];
|
||||||
|
self.width = [NSNumber numberWithInteger:1280];
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
self.height = [NSNumber numberWithInteger:2160];
|
||||||
|
self.width = [NSNumber numberWithInteger:3840];
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
default:
|
||||||
|
self.height = [NSNumber numberWithInteger:1080];
|
||||||
|
self.width = [NSNumber numberWithInteger:1920];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
self.onscreenControls = (NSInteger)OnScreenControlsLevelOff;
|
||||||
|
#else
|
||||||
self.bitrate = settings.bitrate;
|
self.bitrate = settings.bitrate;
|
||||||
self.framerate = settings.framerate;
|
self.framerate = settings.framerate;
|
||||||
self.height = settings.height;
|
self.height = settings.height;
|
||||||
self.width = settings.width;
|
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;
|
self.onscreenControls = settings.onscreenControls;
|
||||||
|
#endif
|
||||||
self.uniqueId = settings.uniqueId;
|
self.uniqueId = settings.uniqueId;
|
||||||
|
self.streamingRemotely = settings.streamingRemotely;
|
||||||
|
|
||||||
return self;
|
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.
|
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+5
-6
@@ -2,18 +2,17 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x",
|
"filename" : "L2_60x60@1x.png",
|
||||||
"filename" : "L2_60x60@1x.png"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x",
|
"filename" : "L2_120x120@2x.png",
|
||||||
"filename" : "L2_120x120@2x.png"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x",
|
"scale" : "3x"
|
||||||
"filename" : "L2_180x180@3x.png"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 12 KiB |
+5
-6
@@ -2,18 +2,17 @@
|
|||||||
"images" : [
|
"images" : [
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "1x",
|
"filename" : "R2_60x60@1x.png",
|
||||||
"filename" : "R2_60x60@1x.png"
|
"scale" : "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "2x",
|
"filename" : "R2_120x120@2x.png",
|
||||||
"filename" : "R2_120x120@2x.png"
|
"scale" : "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "universal",
|
"idiom" : "universal",
|
||||||
"scale" : "3x",
|
"scale" : "3x"
|
||||||
"filename" : "R2_120x120@3x.png"
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info" : {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 8.7 KiB |
@@ -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.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
// Swift
|
// Swift
|
||||||
#import "Moonlight-Swift.h"
|
#if !TARGET_OS_IPHONE
|
||||||
@class Controller;
|
#import "Gamepad.h"
|
||||||
|
#endif
|
||||||
|
#import "StreamConfiguration.h"
|
||||||
|
#import "Controller.h"
|
||||||
|
|
||||||
@class OnScreenControls;
|
@class OnScreenControls;
|
||||||
|
|
||||||
@interface ControllerSupport : NSObject
|
@interface ControllerSupport : NSObject
|
||||||
|
|
||||||
-(id) init;
|
-(id) initWithConfig:(StreamConfiguration*)streamConfig;
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc;
|
||||||
-(void) cleanup;
|
-(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) updateLeftStick:(Controller*)controller x:(short)x y:(short)y;
|
||||||
-(void) updateRightStick:(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) clearButtonFlag:(Controller*)controller flags:(int)flags;
|
||||||
|
|
||||||
-(void) updateFinished:(Controller*)controller;
|
-(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 connectObserver;
|
||||||
@property (nonatomic, strong) id disconnectObserver;
|
@property (nonatomic, strong) id disconnectObserver;
|
||||||
|
|||||||
@@ -7,18 +7,24 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
|
#import "Controller.h"
|
||||||
|
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
|
#if !TARGET_OS_IPHONE
|
||||||
|
#import "Gamepad.h"
|
||||||
|
#import "Control.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#import "DataManager.h"
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
|
|
||||||
// Swift
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
@class Controller;
|
|
||||||
|
|
||||||
@import GameController;
|
@import GameController;
|
||||||
|
@import AudioToolbox;
|
||||||
|
|
||||||
@implementation ControllerSupport {
|
@implementation ControllerSupport {
|
||||||
NSLock *_controllerStreamLock;
|
NSLock *_controllerStreamLock;
|
||||||
NSMutableDictionary *_controllers;
|
NSMutableDictionary *_controllers;
|
||||||
|
NSTimer *_rumbleTimer;
|
||||||
|
|
||||||
OnScreenControls *_osc;
|
OnScreenControls *_osc;
|
||||||
|
|
||||||
@@ -26,16 +32,50 @@
|
|||||||
// and player 0
|
// and player 0
|
||||||
Controller *_player0osc;
|
Controller *_player0osc;
|
||||||
|
|
||||||
char _controllerNumbers;
|
|
||||||
|
|
||||||
#define EMULATING_SELECT 0x1
|
#define EMULATING_SELECT 0x1
|
||||||
#define EMULATING_SPECIAL 0x2
|
#define EMULATING_SPECIAL 0x2
|
||||||
|
|
||||||
|
bool _oscEnabled;
|
||||||
|
char _controllerNumbers;
|
||||||
|
bool _multiController;
|
||||||
}
|
}
|
||||||
|
|
||||||
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
|
// UPDATE_BUTTON_FLAG(controller, flag, pressed)
|
||||||
#define UPDATE_BUTTON_FLAG(controller, x, y) \
|
#define UPDATE_BUTTON_FLAG(controller, x, y) \
|
||||||
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
|
((y) ? [self setButtonFlag:controller flags:x] : [self clearButtonFlag:controller flags:x])
|
||||||
|
|
||||||
|
-(void) rumbleController: (Controller*)controller
|
||||||
|
{
|
||||||
|
// Only vibrate if the amplitude is large enough
|
||||||
|
if (controller.lowFreqMotor > 0x5000 || controller.highFreqMotor > 0x5000) {
|
||||||
|
// If the gamepad is nil (on-screen controls) or it's attached to the device,
|
||||||
|
// then vibrate the device itself
|
||||||
|
if (controller.gamepad == nil || [controller.gamepad isAttachedToDevice]) {
|
||||||
|
AudioServicesPlayAlertSound(kSystemSoundID_Vibrate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor
|
||||||
|
{
|
||||||
|
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:controllerNumber]];
|
||||||
|
if (controller == nil && controllerNumber == 0 && _oscEnabled) {
|
||||||
|
// No physical controller, but we have on-screen controls
|
||||||
|
controller = _player0osc;
|
||||||
|
}
|
||||||
|
if (controller == nil) {
|
||||||
|
// No connected controller for this player
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the motor levels for the rumble timer to grab next iteration
|
||||||
|
controller.lowFreqMotor = lowFreqMotor;
|
||||||
|
controller.highFreqMotor = highFreqMotor;
|
||||||
|
|
||||||
|
// Rumble now to ensure short vibrations aren't missed
|
||||||
|
[self rumbleController:controller];
|
||||||
|
}
|
||||||
|
|
||||||
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
|
-(void) updateLeftStick:(Controller*)controller x:(short)x y:(short)y
|
||||||
{
|
{
|
||||||
@synchronized(controller) {
|
@synchronized(controller) {
|
||||||
@@ -112,17 +152,16 @@
|
|||||||
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
|
-(void) updateButtonFlags:(Controller*)controller flags:(int)flags
|
||||||
{
|
{
|
||||||
@synchronized(controller) {
|
@synchronized(controller) {
|
||||||
int releasedButtons = (controller.lastButtonFlags ^ flags) & ~flags;
|
|
||||||
int pressedButtons = (controller.lastButtonFlags ^ flags) & flags;
|
|
||||||
|
|
||||||
controller.lastButtonFlags = flags;
|
controller.lastButtonFlags = flags;
|
||||||
|
|
||||||
// This must be called before handleSpecialCombosPressed
|
// This must be called before handleSpecialCombosPressed
|
||||||
// because we clear the original button flags there
|
// 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 handleSpecialCombosReleased:controller releasedButtons:releasedButtons];
|
||||||
|
|
||||||
[self handleSpecialCombosPressed:controller pressedButtons:pressedButtons];
|
[self handleSpecialCombosPressed:controller pressedButtons:pressedButtons];
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +186,8 @@
|
|||||||
[_controllerStreamLock lock];
|
[_controllerStreamLock lock];
|
||||||
@synchronized(controller) {
|
@synchronized(controller) {
|
||||||
// Player 1 is always present for OSC
|
// 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];
|
[_controllerStreamLock unlock];
|
||||||
}
|
}
|
||||||
@@ -170,20 +210,24 @@
|
|||||||
{
|
{
|
||||||
if (controller != NULL) {
|
if (controller != NULL) {
|
||||||
controller.controllerPausedHandler = ^(GCController *controller) {
|
controller.controllerPausedHandler = ^(GCController *controller) {
|
||||||
Controller* limeController = [_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
Controller* limeController = [self->_controllers objectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||||
[self setButtonFlag:limeController flags:PLAY_FLAG];
|
|
||||||
[self updateFinished:limeController];
|
|
||||||
|
|
||||||
// Pause for 100 ms
|
// Get off the main thread
|
||||||
usleep(100 * 1000);
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
|
[self setButtonFlag:limeController flags:PLAY_FLAG];
|
||||||
|
[self updateFinished:limeController];
|
||||||
|
|
||||||
[self clearButtonFlag:limeController flags:PLAY_FLAG];
|
// Pause for 100 ms
|
||||||
[self updateFinished:limeController];
|
usleep(100 * 1000);
|
||||||
|
|
||||||
|
[self clearButtonFlag:limeController flags:PLAY_FLAG];
|
||||||
|
[self updateFinished:limeController];
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (controller.extendedGamepad != NULL) {
|
if (controller.extendedGamepad != NULL) {
|
||||||
controller.extendedGamepad.valueChangedHandler = ^(GCExtendedGamepad *gamepad, GCControllerElement *element) {
|
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 leftStickX, leftStickY;
|
||||||
short rightStickX, rightStickY;
|
short rightStickX, rightStickY;
|
||||||
char leftTrigger, rightTrigger;
|
char leftTrigger, rightTrigger;
|
||||||
@@ -201,6 +245,16 @@
|
|||||||
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
|
UPDATE_BUTTON_FLAG(limeController, LB_FLAG, gamepad.leftShoulder.pressed);
|
||||||
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
|
UPDATE_BUTTON_FLAG(limeController, RB_FLAG, gamepad.rightShoulder.pressed);
|
||||||
|
|
||||||
|
// Yay, iOS 12.1 now supports analog stick buttons
|
||||||
|
if (@available(iOS 12.1, tvOS 12.1, *)) {
|
||||||
|
if (gamepad.leftThumbstickButton != nil) {
|
||||||
|
UPDATE_BUTTON_FLAG(limeController, LS_CLK_FLAG, gamepad.leftThumbstickButton.pressed);
|
||||||
|
}
|
||||||
|
if (gamepad.rightThumbstickButton != nil) {
|
||||||
|
UPDATE_BUTTON_FLAG(limeController, RS_CLK_FLAG, gamepad.rightThumbstickButton.pressed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
leftStickX = gamepad.leftThumbstick.xAxis.value * 0x7FFE;
|
||||||
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
leftStickY = gamepad.leftThumbstick.yAxis.value * 0x7FFE;
|
||||||
|
|
||||||
@@ -218,7 +272,7 @@
|
|||||||
}
|
}
|
||||||
else if (controller.gamepad != NULL) {
|
else if (controller.gamepad != NULL) {
|
||||||
controller.gamepad.valueChangedHandler = ^(GCGamepad *gamepad, GCControllerElement *element) {
|
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, A_FLAG, gamepad.buttonA.pressed);
|
||||||
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
|
UPDATE_BUTTON_FLAG(limeController, B_FLAG, gamepad.buttonB.pressed);
|
||||||
UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
|
UPDATE_BUTTON_FLAG(limeController, X_FLAG, gamepad.buttonX.pressed);
|
||||||
@@ -257,6 +311,12 @@
|
|||||||
if (controller != NULL) {
|
if (controller != NULL) {
|
||||||
if (controller.extendedGamepad != NULL) {
|
if (controller.extendedGamepad != NULL) {
|
||||||
level = OnScreenControlsLevelAutoGCExtendedGamepad;
|
level = OnScreenControlsLevelAutoGCExtendedGamepad;
|
||||||
|
if (@available(iOS 12.1, tvOS 12.1, *)) {
|
||||||
|
if (controller.extendedGamepad.leftThumbstickButton != nil &&
|
||||||
|
controller.extendedGamepad.rightThumbstickButton != nil) {
|
||||||
|
level = OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons;
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else if (controller.gamepad != NULL) {
|
else if (controller.gamepad != NULL) {
|
||||||
@@ -266,7 +326,9 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
[_osc setLevel:level];
|
[_osc setLevel:level];
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
|
-(void) initAutoOnScreenControlMode:(OnScreenControls*)osc
|
||||||
@@ -283,6 +345,7 @@
|
|||||||
controller.playerIndex = i;
|
controller.playerIndex = i;
|
||||||
|
|
||||||
Controller* limeController;
|
Controller* limeController;
|
||||||
|
|
||||||
if (i == 0) {
|
if (i == 0) {
|
||||||
// Player 0 shares a controller object with the on-screen controls
|
// Player 0 shares a controller object with the on-screen controls
|
||||||
limeController = _player0osc;
|
limeController = _player0osc;
|
||||||
@@ -291,6 +354,8 @@
|
|||||||
limeController.playerIndex = i;
|
limeController.playerIndex = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
limeController.gamepad = controller;
|
||||||
|
|
||||||
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
||||||
|
|
||||||
Log(LOG_I, @"Assigning controller index: %d", i);
|
Log(LOG_I, @"Assigning controller index: %d", i);
|
||||||
@@ -299,31 +364,153 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
-(Controller*) getOscController {
|
-(Controller*) getOscController {
|
||||||
return _player0osc;
|
return _player0osc;
|
||||||
}
|
}
|
||||||
|
#else
|
||||||
|
-(NSMutableDictionary*) getControllers {
|
||||||
|
return _controllers;
|
||||||
|
}
|
||||||
|
|
||||||
-(id) init
|
-(void) assignGamepad:(struct Gamepad_device *)gamepad {
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
if (!(_controllerNumbers & (1 << i))) {
|
||||||
|
_controllerNumbers |= (1 << i);
|
||||||
|
gamepad->deviceID = i;
|
||||||
|
NSLog(@"Gamepad device id: %u assigned", gamepad->deviceID);
|
||||||
|
Controller* limeController;
|
||||||
|
limeController = [[Controller alloc] init];
|
||||||
|
limeController.playerIndex = i;
|
||||||
|
|
||||||
|
[_controllers setObject:limeController forKey:[NSNumber numberWithInteger:i]];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void) removeGamepad:(struct Gamepad_device *)gamepad {
|
||||||
|
_controllerNumbers &= ~(1 << gamepad->deviceID);
|
||||||
|
Log(LOG_I, @"Unassigning controller index: %ld", (long)gamepad->deviceID);
|
||||||
|
|
||||||
|
// Inform the server of the updated active gamepads before removing this controller
|
||||||
|
[self updateFinished:[_controllers objectForKey:[NSNumber numberWithInteger:gamepad->deviceID]]];
|
||||||
|
[_controllers removeObjectForKey:[NSNumber numberWithInteger:gamepad->deviceID]];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
+(bool) isSupportedGamepad:(GCController*) controller {
|
||||||
|
return controller.extendedGamepad != nil || controller.gamepad != nil;
|
||||||
|
}
|
||||||
|
|
||||||
|
+(int) getGamepadCount {
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (GCController* controller in [GCController controllers]) {
|
||||||
|
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
+(int) getConnectedGamepadMask:(StreamConfiguration*)streamConfig {
|
||||||
|
int mask = 0;
|
||||||
|
|
||||||
|
if (streamConfig.multiController) {
|
||||||
|
int i = 0;
|
||||||
|
for (GCController* controller in [GCController controllers]) {
|
||||||
|
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||||
|
mask |= 1 << i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Some games don't deal with having controller reconnected
|
||||||
|
// properly so always report controller 1 if not in MC mode
|
||||||
|
mask = 0x1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
|
OnScreenControlsLevel level = (OnScreenControlsLevel)[[dataMan getSettings].onscreenControls integerValue];
|
||||||
|
|
||||||
|
// Even if no gamepads are present, we will always count one
|
||||||
|
// if OSC is enabled.
|
||||||
|
if (level != OnScreenControlsLevelOff) {
|
||||||
|
mask |= 0x1;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return mask;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void) rumbleTimer
|
||||||
|
{
|
||||||
|
for (int i = 0; i < 4; i++) {
|
||||||
|
Controller* controller = [_controllers objectForKey:[NSNumber numberWithInteger:i]];
|
||||||
|
if (controller == nil && i == 0 && _oscEnabled) {
|
||||||
|
// No physical controller, but we have on-screen controls
|
||||||
|
controller = _player0osc;
|
||||||
|
}
|
||||||
|
if (controller == nil) {
|
||||||
|
// No connected controller for this player
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
[self rumbleController:controller];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
-(id) initWithConfig:(StreamConfiguration*)streamConfig
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
_controllerStreamLock = [[NSLock alloc] init];
|
_controllerStreamLock = [[NSLock alloc] init];
|
||||||
_controllers = [[NSMutableDictionary alloc] init];
|
_controllers = [[NSMutableDictionary alloc] init];
|
||||||
_controllerNumbers = 0;
|
_controllerNumbers = 0;
|
||||||
|
_multiController = streamConfig.multiController;
|
||||||
|
|
||||||
_player0osc = [[Controller alloc] init];
|
_player0osc = [[Controller alloc] init];
|
||||||
_player0osc.playerIndex = 0;
|
_player0osc.playerIndex = 0;
|
||||||
|
|
||||||
Log(LOG_I, @"Number of controllers connected: %ld", (long)[[GCController controllers] count]);
|
#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);
|
||||||
|
|
||||||
for (GCController* controller in [GCController controllers]) {
|
for (GCController* controller in [GCController controllers]) {
|
||||||
[self assignController:controller];
|
if ([ControllerSupport isSupportedGamepad:controller]) {
|
||||||
[self registerControllerCallbacks:controller];
|
[self assignController:controller];
|
||||||
[self updateAutoOnScreenControlMode];
|
[self registerControllerCallbacks:controller];
|
||||||
|
[self updateAutoOnScreenControlMode];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
self.connectObserver = [[NSNotificationCenter defaultCenter] addObserverForName:GCControllerDidConnectNotification object:nil queue:[NSOperationQueue mainQueue] usingBlock:^(NSNotification *note) {
|
||||||
Log(LOG_I, @"Controller connected!");
|
Log(LOG_I, @"Controller connected!");
|
||||||
|
|
||||||
GCController* controller = note.object;
|
GCController* controller = note.object;
|
||||||
|
|
||||||
|
if (![ControllerSupport isSupportedGamepad:controller]) {
|
||||||
|
// Ignore micro gamepads and motion controllers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[self assignController:controller];
|
[self assignController:controller];
|
||||||
|
|
||||||
// Register callbacks on the new controller
|
// Register callbacks on the new controller
|
||||||
@@ -336,26 +523,42 @@
|
|||||||
Log(LOG_I, @"Controller disconnected!");
|
Log(LOG_I, @"Controller disconnected!");
|
||||||
|
|
||||||
GCController* controller = note.object;
|
GCController* controller = note.object;
|
||||||
|
|
||||||
|
if (![ControllerSupport isSupportedGamepad:controller]) {
|
||||||
|
// Ignore micro gamepads and motion controllers
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
[self unregisterControllerCallbacks:controller];
|
[self unregisterControllerCallbacks:controller];
|
||||||
[_controllers removeObjectForKey:[NSNumber numberWithInteger:controller.playerIndex]];
|
self->_controllerNumbers &= ~(1 << controller.playerIndex);
|
||||||
_controllerNumbers &= ~(1 << controller.playerIndex);
|
|
||||||
Log(LOG_I, @"Unassigning controller index: %ld", (long)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
|
// Re-evaluate the on-screen control mode
|
||||||
[self updateAutoOnScreenControlMode];
|
[self updateAutoOnScreenControlMode];
|
||||||
}];
|
}];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
-(void) cleanup
|
-(void) cleanup
|
||||||
{
|
{
|
||||||
|
[_rumbleTimer invalidate];
|
||||||
|
_rumbleTimer = nil;
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
[[NSNotificationCenter defaultCenter] removeObserver:self.connectObserver];
|
||||||
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
[[NSNotificationCenter defaultCenter] removeObserver:self.disconnectObserver];
|
||||||
[_controllers removeAllObjects];
|
[_controllers removeAllObjects];
|
||||||
_controllerNumbers = 0;
|
_controllerNumbers = 0;
|
||||||
for (GCController* controller in [GCController controllers]) {
|
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
|
// Internal levels selected by ControllerSupport
|
||||||
OnScreenControlsLevelAutoGCGamepad,
|
OnScreenControlsLevelAutoGCGamepad,
|
||||||
OnScreenControlsLevelAutoGCExtendedGamepad,
|
OnScreenControlsLevelAutoGCExtendedGamepad,
|
||||||
|
OnScreenControlsLevelAutoGCExtendedGamepadWithStickButtons
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
|
- (id) initWithView:(UIView*)view controllerSup:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)edgeDelegate;
|
||||||
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
- (BOOL) handleTouchDownEvent:(NSSet*)touches;
|
||||||
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
- (BOOL) handleTouchUpEvent:(NSSet*)touches;
|
||||||
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
- (BOOL) handleTouchMovedEvent:(NSSet*)touches;
|
||||||
- (void) setLevel:(OnScreenControlsLevel)level;
|
- (void) setLevel:(OnScreenControlsLevel)level;
|
||||||
|
#endif
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -8,12 +8,9 @@
|
|||||||
|
|
||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
//#import "Controller.h"
|
#import "Controller.h"
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
|
|
||||||
#import "Moonlight-Swift.h"
|
|
||||||
@class Controller;
|
|
||||||
|
|
||||||
#define UPDATE_BUTTON(x, y) (buttonFlags = \
|
#define UPDATE_BUTTON(x, y) (buttonFlags = \
|
||||||
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
|
(y) ? (buttonFlags | (x)) : (buttonFlags & ~(x)))
|
||||||
|
|
||||||
@@ -202,15 +199,27 @@ static float L3_Y;
|
|||||||
[self hideSticks];
|
[self hideSticks];
|
||||||
[self drawL3R3];
|
[self drawL3R3];
|
||||||
break;
|
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:
|
case OnScreenControlsLevelSimple:
|
||||||
[self setupSimpleControls];
|
[self setupSimpleControls];
|
||||||
|
|
||||||
[self drawTriggers];
|
[self hideTriggers];
|
||||||
[self drawStartSelect];
|
[self hideL3R3];
|
||||||
[self drawL3R3];
|
|
||||||
[self hideButtons];
|
|
||||||
[self hideBumpers];
|
[self hideBumpers];
|
||||||
[self hideSticks];
|
[self hideSticks];
|
||||||
|
[self drawStartSelect];
|
||||||
|
[self drawButtons];
|
||||||
break;
|
break;
|
||||||
case OnScreenControlsLevelFull:
|
case OnScreenControlsLevelFull:
|
||||||
[self setupComplexControls];
|
[self setupComplexControls];
|
||||||
@@ -238,8 +247,22 @@ static float L3_Y;
|
|||||||
// Start with the default complex layout
|
// Start with the default complex layout
|
||||||
[self setupComplexControls];
|
[self setupComplexControls];
|
||||||
|
|
||||||
|
START_X = _controlArea.size.width * .95 + _controlArea.origin.x;
|
||||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
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;
|
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
|
// For GCGamepad controls we move triggers, start, and select
|
||||||
@@ -248,10 +271,30 @@ static float L3_Y;
|
|||||||
// Start with the default complex layout
|
// Start with the default complex layout
|
||||||
[self setupComplexControls];
|
[self setupComplexControls];
|
||||||
|
|
||||||
START_X = _controlArea.size.width * .6 + _controlArea.origin.x;
|
L2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
L2_X = _controlArea.size.width * .05 + _controlArea.origin.x;
|
||||||
|
|
||||||
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
R2_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||||
|
R2_X = _controlArea.size.width * .95 + _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) {
|
||||||
|
// The analog sticks are kept closer to the sides on iPad
|
||||||
|
LS_CENTER_X = _controlArea.size.width * .15 + _controlArea.origin.x;
|
||||||
|
RS_CENTER_X = _controlArea.size.width * .85 + _controlArea.origin.x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For simple controls we move the triggers and buttons to the bottom
|
||||||
|
- (void) setupSimpleControls {
|
||||||
|
// Start with the default complex layout
|
||||||
|
[self setupComplexControls];
|
||||||
|
|
||||||
|
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||||
|
|
||||||
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
L2_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
||||||
@@ -261,47 +304,18 @@ static float L3_Y;
|
|||||||
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
R2_X = _controlArea.size.width * .9 + _controlArea.origin.x;
|
||||||
|
|
||||||
if (_iPad) {
|
if (_iPad) {
|
||||||
START_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
// Lower the D-pad and buttons on iPad
|
||||||
START_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
D_PAD_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||||
|
BUTTON_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
||||||
|
|
||||||
SELECT_X = _controlArea.size.width * .25 + _controlArea.origin.x;
|
// Move Start and Select closer to sides
|
||||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
SELECT_X = _controlArea.size.width * .2 + _controlArea.origin.x;
|
||||||
|
START_X = _controlArea.size.width * .8 + _controlArea.origin.x;
|
||||||
// The analog sticks are kept closer to the sides on iPad
|
}
|
||||||
LS_CENTER_X = _controlArea.size.width * .18 + _controlArea.origin.x;
|
else {
|
||||||
LS_CENTER_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
SELECT_X = _controlArea.size.width * .4 + _controlArea.origin.x;
|
||||||
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_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;
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// For simple controls we move the triggers and buttons to the bottom
|
|
||||||
- (void) setupSimpleControls {
|
|
||||||
// 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;
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) setupComplexControls
|
- (void) setupComplexControls
|
||||||
@@ -315,9 +329,9 @@ static float L3_Y;
|
|||||||
{
|
{
|
||||||
// The analog sticks are kept closer to the sides on iPad
|
// The analog sticks are kept closer to the sides on iPad
|
||||||
LS_CENTER_X = _controlArea.size.width * .22 + _controlArea.origin.x;
|
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_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
|
else
|
||||||
{
|
{
|
||||||
@@ -332,18 +346,24 @@ static float L3_Y;
|
|||||||
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
SELECT_X = _controlArea.size.width * .1 + _controlArea.origin.x;
|
||||||
SELECT_Y = _controlArea.size.height * .9 + _controlArea.origin.y;
|
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;
|
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;
|
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;
|
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;
|
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;
|
if (_iPad) {
|
||||||
R3_X = _controlArea.size.width * .75 + _controlArea.origin.x;
|
// Move L/R buttons closer to the side on iPad
|
||||||
R3_Y = _controlArea.size.height * .75 + _controlArea.origin.y;
|
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 {
|
- (void) drawButtons {
|
||||||
@@ -836,26 +856,31 @@ static float L3_Y;
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL) isInDeadZone:(UITouch*) touch {
|
- (BOOL) isInDeadZone:(UITouch*) touch {
|
||||||
switch (_level) {
|
// Dynamically evaluate deadzones based on the controls
|
||||||
case OnScreenControlsLevelFull:
|
// on screen at the time
|
||||||
return [self isDpadDeadZone:touch]
|
if (_leftButton.superlayer != nil && [self isDpadDeadZone:touch]) {
|
||||||
|| [self isAbxyDeadZone:touch]
|
return true;
|
||||||
|| [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;
|
|
||||||
}
|
}
|
||||||
|
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 {
|
- (BOOL) isDpadDeadZone:(UITouch*) touch {
|
||||||
@@ -926,6 +951,19 @@ static float L3_Y;
|
|||||||
endY:_view.frame.origin.y + _view.frame.size.height];
|
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 {
|
- (BOOL) isDeadZone:(UITouch*) touch startX:(float)deadZoneStartX startY:(float)deadZoneStartY endX:(float)deadZoneEndX endY:(float)deadZoneEndY {
|
||||||
deadZoneStartX -= DEAD_ZONE_PADDING;
|
deadZoneStartX -= DEAD_ZONE_PADDING;
|
||||||
deadZoneStartY -= DEAD_ZONE_PADDING;
|
deadZoneStartY -= DEAD_ZONE_PADDING;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <UIKit/UIKit.h>
|
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
|
|
||||||
@protocol EdgeDetectionDelegate <NSObject>
|
@protocol EdgeDetectionDelegate <NSObject>
|
||||||
@@ -15,7 +14,9 @@
|
|||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface StreamView : UIView
|
@interface StreamView : OSView <UITextFieldDelegate>
|
||||||
|
|
||||||
|
@property (nonatomic, retain) IBOutlet UITextField* keyInputField;
|
||||||
|
|
||||||
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
|
- (void) setupOnScreenControls:(ControllerSupport*)controllerSupport swipeDelegate:(id<EdgeDetectionDelegate>)swipeDelegate;
|
||||||
- (void) setMouseDeltaFactors:(float)x y:(float)y;
|
- (void) setMouseDeltaFactors:(float)x y:(float)y;
|
||||||
|
|||||||
+158
-23
@@ -11,18 +11,22 @@
|
|||||||
#import "OnScreenControls.h"
|
#import "OnScreenControls.h"
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
#import "ControllerSupport.h"
|
#import "ControllerSupport.h"
|
||||||
|
#import "KeyboardSupport.h"
|
||||||
|
|
||||||
@implementation StreamView {
|
@implementation StreamView {
|
||||||
CGPoint touchLocation, originalLocation;
|
CGPoint touchLocation, originalLocation;
|
||||||
BOOL touchMoved;
|
BOOL touchMoved;
|
||||||
OnScreenControls* onScreenControls;
|
OnScreenControls* onScreenControls;
|
||||||
|
|
||||||
|
BOOL isInputingText;
|
||||||
BOOL isDragging;
|
BOOL isDragging;
|
||||||
NSTimer* dragTimer;
|
NSTimer* dragTimer;
|
||||||
|
|
||||||
float xDeltaFactor;
|
float xDeltaFactor;
|
||||||
float yDeltaFactor;
|
float yDeltaFactor;
|
||||||
float screenFactor;
|
float screenFactor;
|
||||||
|
|
||||||
|
NSDictionary<NSString *, NSNumber *> *dictCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) setMouseDeltaFactors:(float)x y:(float)y {
|
- (void) setMouseDeltaFactors:(float)x y:(float)y {
|
||||||
@@ -44,11 +48,12 @@
|
|||||||
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
Log(LOG_I, @"Setting manual on-screen controls level: %d", (int)level);
|
||||||
[onScreenControls setLevel:level];
|
[onScreenControls setLevel:level];
|
||||||
}
|
}
|
||||||
|
[self becomeFirstResponder];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
- (Boolean)isConfirmedMove:(CGPoint)currentPoint from:(CGPoint)originalPoint {
|
||||||
// Movements of greater than 20 pixels are considered confirmed
|
// Movements of greater than 10 pixels are considered confirmed
|
||||||
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 20;
|
return hypotf(originalPoint.x - currentPoint.x, originalPoint.y - currentPoint.y) >= 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
@@ -76,8 +81,6 @@
|
|||||||
|
|
||||||
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
if (![onScreenControls handleTouchMovedEvent:touches]) {
|
||||||
[dragTimer invalidate];
|
|
||||||
dragTimer = nil;
|
|
||||||
if ([[event allTouches] count] == 1) {
|
if ([[event allTouches] count] == 1) {
|
||||||
UITouch *touch = [[event allTouches] anyObject];
|
UITouch *touch = [[event allTouches] anyObject];
|
||||||
CGPoint currentLocation = [touch locationInView:self];
|
CGPoint currentLocation = [touch locationInView:self];
|
||||||
@@ -128,33 +131,54 @@
|
|||||||
if (![onScreenControls handleTouchUpEvent:touches]) {
|
if (![onScreenControls handleTouchUpEvent:touches]) {
|
||||||
[dragTimer invalidate];
|
[dragTimer invalidate];
|
||||||
dragTimer = nil;
|
dragTimer = nil;
|
||||||
if (!touchMoved) {
|
if (isDragging) {
|
||||||
if ([[event allTouches] count] == 2) {
|
isDragging = false;
|
||||||
Log(LOG_D, @"Sending right mouse button press");
|
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_RIGHT);
|
// Undo causes issues for our state management, so turn it off
|
||||||
|
[_keyInputField.undoManager disableUndoRegistration];
|
||||||
|
|
||||||
// Wait 100 ms to simulate a real button press
|
isInputingText = true;
|
||||||
usleep(100 * 1000);
|
}
|
||||||
|
} 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_RELEASE, BUTTON_RIGHT);
|
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_RIGHT);
|
||||||
} else {
|
|
||||||
if (!isDragging){
|
|
||||||
Log(LOG_D, @"Sending left mouse button press");
|
|
||||||
|
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_PRESS, BUTTON_LEFT);
|
|
||||||
|
|
||||||
// Wait 100 ms to simulate a real button press
|
// Wait 100 ms to simulate a real button press
|
||||||
usleep(100 * 1000);
|
usleep(100 * 1000);
|
||||||
}
|
|
||||||
isDragging = false;
|
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_RIGHT);
|
||||||
LiSendMouseButtonEvent(BUTTON_ACTION_RELEASE, BUTTON_LEFT);
|
});
|
||||||
|
} 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
|
// 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
|
// of the active finger so we don't jump unexpectedly on the next touchesMoved
|
||||||
@@ -175,5 +199,116 @@
|
|||||||
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
||||||
|
// This method is called when the "Return" key is pressed.
|
||||||
|
LiSendKeyboardEvent(0x0d, KEY_ACTION_DOWN, 0);
|
||||||
|
usleep(50 * 1000);
|
||||||
|
LiSendKeyboardEvent(0x0d, KEY_ACTION_UP, 0);
|
||||||
|
return NO;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)onKeyboardPressed:(UITextField *)textField {
|
||||||
|
NSString* inputText = textField.text;
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
|
// If the text became empty, we know the user pressed the backspace key.
|
||||||
|
if ([inputText isEqual:@""]) {
|
||||||
|
LiSendKeyboardEvent(0x08, KEY_ACTION_DOWN, 0);
|
||||||
|
usleep(50 * 1000);
|
||||||
|
LiSendKeyboardEvent(0x08, KEY_ACTION_UP, 0);
|
||||||
|
} else {
|
||||||
|
// Character 0 will be our known sentinel value
|
||||||
|
for (int i = 1; i < [inputText length]; i++) {
|
||||||
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[inputText characterAtIndex:i] withModifierFlags:0];
|
||||||
|
if (event.keycode == 0) {
|
||||||
|
// If we don't know the code, don't send anything.
|
||||||
|
Log(LOG_W, @"Unknown key code: [%c]", [inputText characterAtIndex:i]);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
[self sendLowLevelEvent:event];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Reset text field back to known state
|
||||||
|
textField.text = @"0";
|
||||||
|
|
||||||
|
// Move the insertion point back to the end of the text box
|
||||||
|
UITextRange *textRange = [textField textRangeFromPosition:textField.endOfDocument toPosition:textField.endOfDocument];
|
||||||
|
[textField setSelectedTextRange:textRange];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)specialCharPressed:(UIKeyCommand *)cmd {
|
||||||
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:0x20 withModifierFlags:[cmd modifierFlags]];
|
||||||
|
event.keycode = [[dictCodes valueForKey:[cmd input]] intValue];
|
||||||
|
[self sendLowLevelEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)keyPressed:(UIKeyCommand *)cmd {
|
||||||
|
struct KeyEvent event = [KeyboardSupport translateKeyEvent:[[cmd input] characterAtIndex:0] withModifierFlags:[cmd modifierFlags]];
|
||||||
|
[self sendLowLevelEvent:event];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void)sendLowLevelEvent:(struct KeyEvent)event {
|
||||||
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
||||||
|
// When we want to send a modified key (like uppercase letters) we need to send the
|
||||||
|
// modifier ("shift") seperately from the key itself.
|
||||||
|
if (event.modifier != 0) {
|
||||||
|
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_DOWN, event.modifier);
|
||||||
|
}
|
||||||
|
LiSendKeyboardEvent(event.keycode, KEY_ACTION_DOWN, event.modifier);
|
||||||
|
usleep(50 * 1000);
|
||||||
|
LiSendKeyboardEvent(event.keycode, KEY_ACTION_UP, event.modifier);
|
||||||
|
if (event.modifier != 0) {
|
||||||
|
LiSendKeyboardEvent(event.modifierKeycode, KEY_ACTION_UP, event.modifier);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)canBecomeFirstResponder {
|
||||||
|
return YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
- (NSArray<UIKeyCommand *> *)keyCommands
|
||||||
|
{
|
||||||
|
NSString *charset = @"qwertyuiopasdfghjklzxcvbnm1234567890\t§[]\\'\"/.,`<>-´ç+`¡'º;ñ= ";
|
||||||
|
|
||||||
|
NSMutableArray<UIKeyCommand *> * commands = [NSMutableArray<UIKeyCommand *> array];
|
||||||
|
dictCodes = [[NSDictionary alloc] initWithObjectsAndKeys: [NSNumber numberWithInt: 0x0d], @"\r", [NSNumber numberWithInt: 0x08], @"\b", [NSNumber numberWithInt: 0x1b], UIKeyInputEscape, [NSNumber numberWithInt: 0x28], UIKeyInputDownArrow, [NSNumber numberWithInt: 0x26], UIKeyInputUpArrow, [NSNumber numberWithInt: 0x25], UIKeyInputLeftArrow, [NSNumber numberWithInt: 0x27], UIKeyInputRightArrow, nil];
|
||||||
|
|
||||||
|
[charset enumerateSubstringsInRange:NSMakeRange(0, charset.length)
|
||||||
|
options:NSStringEnumerationByComposedCharacterSequences
|
||||||
|
usingBlock:^(NSString *substring, NSRange substringRange, NSRange enclosingRange, BOOL *stop) {
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:0 action:@selector(keyPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierShift action:@selector(keyPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierControl action:@selector(keyPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:substring modifierFlags:UIKeyModifierAlternate action:@selector(keyPressed:)]];
|
||||||
|
}];
|
||||||
|
|
||||||
|
for (NSString *c in [dictCodes keyEnumerator]) {
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:0
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierShift
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierShift | UIKeyModifierAlternate
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierShift | UIKeyModifierControl
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierControl
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierControl | UIKeyModifierAlternate
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
[commands addObject:[UIKeyCommand keyCommandWithInput:c
|
||||||
|
modifierFlags:UIKeyModifierAlternate
|
||||||
|
action:@selector(specialCharPressed:)]];
|
||||||
|
}
|
||||||
|
|
||||||
|
return commands;
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>CADisableMinimumFrameDuration</key>
|
||||||
|
<true/>
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
@@ -17,11 +19,24 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.4.0</string>
|
<string>2.6.0</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>2</string>
|
<string>1</string>
|
||||||
|
<key>GCSupportedGameControllers</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>ProfileName</key>
|
||||||
|
<string>ExtendedGamepad</string>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>ProfileName</key>
|
||||||
|
<string>Gamepad</string>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
<key>GCSupportsControllerUserInteraction</key>
|
||||||
|
<true/>
|
||||||
<key>LSApplicationCategoryType</key>
|
<key>LSApplicationCategoryType</key>
|
||||||
<string>public.app-category.games</string>
|
<string>public.app-category.games</string>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
@@ -31,15 +46,6 @@
|
|||||||
<key>NSAllowsArbitraryLoads</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<true/>
|
<true/>
|
||||||
</dict>
|
</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>
|
<key>UILaunchStoryboardName</key>
|
||||||
<string>Launch Screen</string>
|
<string>Launch Screen</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>UIMainStoryboardFile</key>
|
||||||
|
|||||||
@@ -5,14 +5,21 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import <Availability.h>
|
#import <Availability.h>
|
||||||
|
#include <TargetConditionals.h>
|
||||||
|
|
||||||
#ifndef __IPHONE_3_0
|
#ifndef __IPHONE_3_0
|
||||||
#warning "This project uses features only available in iOS SDK 3.0 and later."
|
#warning "This project uses features only available in iOS SDK 3.0 and later."
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
#ifdef __OBJC__
|
#ifdef __OBJC__
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
#import <UIKit/UIKit.h>
|
#import <UIKit/UIKit.h>
|
||||||
#import <Foundation/Foundation.h>
|
#elif TARGET_OS_MAC
|
||||||
#import <CoreData/CoreData.h>
|
#import <AppKit/AppKit.h>
|
||||||
#import "Logger.h"
|
#endif
|
||||||
|
#import <Foundation/Foundation.h>
|
||||||
|
#import <CoreData/CoreData.h>
|
||||||
|
#import "Logger.h"
|
||||||
|
#include "OSPortabilityDefs.h"
|
||||||
#endif
|
#endif
|
||||||
@@ -3,6 +3,6 @@
|
|||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>_XCCurrentVersionName</key>
|
<key>_XCCurrentVersionName</key>
|
||||||
<string>Moonlight v1.0-2.xcdatamodel</string>
|
<string>Moonlight v1.3.xcdatamodel</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?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">
|
<entity name="App" representedClassName="App" syncable="YES">
|
||||||
<attribute name="id" attributeType="String" syncable="YES"/>
|
<attribute name="id" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="image" optional="YES" attributeType="Binary" allowsExternalBinaryDataStorage="YES" 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="localAddress" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
<attribute name="mac" optional="YES" attributeType="String" syncable="YES"/>
|
||||||
<attribute name="name" 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"/>
|
<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"/>
|
<relationship name="appList" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="App" inverseName="host" inverseEntity="App" syncable="YES"/>
|
||||||
</entity>
|
</entity>
|
||||||
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
<entity name="Settings" representedClassName="Settings" syncable="YES">
|
||||||
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" syncable="YES"/>
|
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" syncable="YES"/>
|
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="height" attributeType="Integer 32" defaultValueString="720" syncable="YES"/>
|
<attribute name="height" attributeType="Integer 32" defaultValueString="720" usesScalarValueType="NO" syncable="YES"/>
|
||||||
<attribute name="onscreenControls" attributeType="Integer 32" defaultValueString="1" 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="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>
|
</entity>
|
||||||
<elements>
|
<elements>
|
||||||
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
<element name="App" positionX="0" positionY="54" width="128" height="105"/>
|
||||||
<element name="Host" positionX="0" positionY="0" width="128" height="163"/>
|
<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>
|
</elements>
|
||||||
</model>
|
</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>
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
#import "HttpManager.h"
|
#import "HttpManager.h"
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
@@ -22,5 +21,6 @@
|
|||||||
- (id) initWithCallback:(id<AppAssetCallback>)callback;
|
- (id) initWithCallback:(id<AppAssetCallback>)callback;
|
||||||
- (void) retrieveAssetsFromHost:(TemporaryHost*)host;
|
- (void) retrieveAssetsFromHost:(TemporaryHost*)host;
|
||||||
- (void) stopRetrieving;
|
- (void) stopRetrieving;
|
||||||
|
+ (NSString*) boxArtPathForApp:(TemporaryApp*)app;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -19,6 +19,22 @@
|
|||||||
|
|
||||||
static const int MAX_REQUEST_COUNT = 4;
|
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 {
|
- (id) initWithCallback:(id<AppAssetCallback>)callback {
|
||||||
self = [super init];
|
self = [super init];
|
||||||
_callback = callback;
|
_callback = callback;
|
||||||
@@ -30,7 +46,7 @@ static const int MAX_REQUEST_COUNT = 4;
|
|||||||
|
|
||||||
- (void) retrieveAssetsFromHost:(TemporaryHost*)host {
|
- (void) retrieveAssetsFromHost:(TemporaryHost*)host {
|
||||||
for (TemporaryApp* app in host.appList) {
|
for (TemporaryApp* app in host.appList) {
|
||||||
if (app.image == nil) {
|
if (![[NSFileManager defaultManager] fileExistsAtPath:[AppAssetManager boxArtPathForApp:app]]) {
|
||||||
AppAssetRetriever* retriever = [[AppAssetRetriever alloc] init];
|
AppAssetRetriever* retriever = [[AppAssetRetriever alloc] init];
|
||||||
retriever.app = app;
|
retriever.app = app;
|
||||||
retriever.host = host;
|
retriever.host = host;
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "HttpResponse.h"
|
#import "HttpResponse.h"
|
||||||
|
|
||||||
@interface AppAssetResponse : NSObject <Response>
|
@interface AppAssetResponse : NSObject <Response>
|
||||||
|
|||||||
@@ -16,9 +16,12 @@
|
|||||||
self.statusMessage = @"App asset has no status message";
|
self.statusMessage = @"App asset has no status message";
|
||||||
self.statusCode = -1;
|
self.statusCode = -1;
|
||||||
}
|
}
|
||||||
|
- (OSImage*) getImage {
|
||||||
- (UIImage*) getImage {
|
#if TARGET_OS_IPHONE
|
||||||
UIImage* appImage = [[UIImage alloc] initWithData:self.data];
|
OSImage* appImage = [[OSImage alloc] initWithData:self.data];
|
||||||
|
#else
|
||||||
|
OSImage* appImage = nil;
|
||||||
|
#endif
|
||||||
return appImage;
|
return appImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
#import "TemporaryApp.h"
|
#import "TemporaryApp.h"
|
||||||
#import "AppAssetManager.h"
|
#import "AppAssetManager.h"
|
||||||
|
|||||||
@@ -18,18 +18,24 @@ static const double RETRY_DELAY = 2; // seconds
|
|||||||
static const int MAX_ATTEMPTS = 5;
|
static const int MAX_ATTEMPTS = 5;
|
||||||
|
|
||||||
- (void) main {
|
- (void) main {
|
||||||
UIImage* appImage = nil;
|
|
||||||
int attempts = 0;
|
int attempts = 0;
|
||||||
while (![self isCancelled] && appImage == nil && attempts++ < MAX_ATTEMPTS) {
|
while (![self isCancelled] && attempts++ < MAX_ATTEMPTS) {
|
||||||
|
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] serverCert:_host.serverCert];
|
||||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_host.activeAddress uniqueId:[IdManager getUniqueId] deviceName:deviceName cert:[CryptoManager readCertFromFile]];
|
|
||||||
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
|
AppAssetResponse* appAssetResp = [[AppAssetResponse alloc] init];
|
||||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:appAssetResp withUrlRequest:[hMan newAppAssetRequestWithAppId:self.app.id]]];
|
||||||
|
|
||||||
appImage = [UIImage imageWithData:appAssetResp.data];
|
#if TARGET_OS_IPHONE
|
||||||
self.app.image = UIImagePNGRepresentation(appImage);
|
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
|
||||||
|
|
||||||
if (![self isCancelled] && appImage == nil) {
|
#endif
|
||||||
|
|
||||||
|
if (![self isCancelled]) {
|
||||||
[NSThread sleepForTimeInterval:RETRY_DELAY];
|
[NSThread sleepForTimeInterval:RETRY_DELAY];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@
|
|||||||
static const char* TAG_APP = "App";
|
static const char* TAG_APP = "App";
|
||||||
static const char* TAG_APP_TITLE = "AppTitle";
|
static const char* TAG_APP_TITLE = "AppTitle";
|
||||||
static const char* TAG_APP_ID = "ID";
|
static const char* TAG_APP_ID = "ID";
|
||||||
|
static const char* TAG_HDR_SUPPORTED = "IsHdrSupported";
|
||||||
|
|
||||||
- (void)populateWithData:(NSData *)xml {
|
- (void)populateWithData:(NSData *)xml {
|
||||||
self.data = xml;
|
self.data = xml;
|
||||||
@@ -66,14 +67,13 @@ static const char* TAG_APP_ID = "ID";
|
|||||||
xmlNodePtr appInfoNode = node->xmlChildrenNode;
|
xmlNodePtr appInfoNode = node->xmlChildrenNode;
|
||||||
NSString* appName = @"";
|
NSString* appName = @"";
|
||||||
NSString* appId = nil;
|
NSString* appId = nil;
|
||||||
|
NSString* hdrSupported = @"0";
|
||||||
while (appInfoNode != NULL) {
|
while (appInfoNode != NULL) {
|
||||||
if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_TITLE)) {
|
if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_TITLE)) {
|
||||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||||
if (nodeVal != NULL) {
|
if (nodeVal != NULL) {
|
||||||
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||||
xmlFree(nodeVal);
|
xmlFree(nodeVal);
|
||||||
} else {
|
|
||||||
appName = @"";
|
|
||||||
}
|
}
|
||||||
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_ID)) {
|
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_APP_ID)) {
|
||||||
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||||
@@ -81,6 +81,12 @@ static const char* TAG_APP_ID = "ID";
|
|||||||
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||||
xmlFree(nodeVal);
|
xmlFree(nodeVal);
|
||||||
}
|
}
|
||||||
|
} else if (!xmlStrcmp(appInfoNode->name, (xmlChar*)TAG_HDR_SUPPORTED)) {
|
||||||
|
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
|
||||||
|
if (nodeVal != NULL) {
|
||||||
|
hdrSupported = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
|
||||||
|
xmlFree(nodeVal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
appInfoNode = appInfoNode->next;
|
appInfoNode = appInfoNode->next;
|
||||||
}
|
}
|
||||||
@@ -88,6 +94,7 @@ static const char* TAG_APP_ID = "ID";
|
|||||||
TemporaryApp* app = [[TemporaryApp alloc] init];
|
TemporaryApp* app = [[TemporaryApp alloc] init];
|
||||||
app.name = appName;
|
app.name = appName;
|
||||||
app.id = appId;
|
app.id = appId;
|
||||||
|
app.hdrSupported = [hdrSupported intValue] != 0;
|
||||||
[_appList addObject:app];
|
[_appList addObject:app];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "MDNSManager.h"
|
#import "MDNSManager.h"
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
|
|
||||||
|
|||||||
@@ -41,16 +41,16 @@
|
|||||||
|
|
||||||
_opQueue = [[NSOperationQueue alloc] init];
|
_opQueue = [[NSOperationQueue alloc] init];
|
||||||
_mdnsMan = [[MDNSManager alloc] initWithCallback:self];
|
_mdnsMan = [[MDNSManager alloc] initWithCallback:self];
|
||||||
[CryptoManager generateKeyPairUsingSSl];
|
[CryptoManager generateKeyPairUsingSSL];
|
||||||
_uniqueId = [IdManager getUniqueId];
|
_uniqueId = [IdManager getUniqueId];
|
||||||
_cert = [CryptoManager readCertFromFile];
|
_cert = [CryptoManager readCertFromFile];
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) discoverHost:(NSString *)hostAddress withCallback:(void (^)(TemporaryHost *, NSString*))callback {
|
- (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];
|
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;
|
TemporaryHost* host = nil;
|
||||||
if ([serverInfoResponse isStatusOk]) {
|
if ([serverInfoResponse isStatusOk]) {
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
host.online = YES;
|
host.online = YES;
|
||||||
[serverInfoResponse populateHost:host];
|
[serverInfoResponse populateHost:host];
|
||||||
if (![self addHostToDiscovery:host]) {
|
if (![self addHostToDiscovery:host]) {
|
||||||
callback(nil, @"Host already added");
|
callback(nil, @"Host information updated");
|
||||||
} else {
|
} else {
|
||||||
callback(host, nil);
|
callback(host, nil);
|
||||||
}
|
}
|
||||||
@@ -70,6 +70,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void) startDiscovery {
|
- (void) startDiscovery {
|
||||||
|
if (shouldDiscover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log(LOG_I, @"Starting discovery");
|
Log(LOG_I, @"Starting discovery");
|
||||||
shouldDiscover = YES;
|
shouldDiscover = YES;
|
||||||
[_mdnsMan searchForHosts];
|
[_mdnsMan searchForHosts];
|
||||||
@@ -79,6 +83,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void) stopDiscovery {
|
- (void) stopDiscovery {
|
||||||
|
if (!shouldDiscover) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log(LOG_I, @"Stopping discovery");
|
Log(LOG_I, @"Stopping discovery");
|
||||||
shouldDiscover = NO;
|
shouldDiscover = NO;
|
||||||
[_mdnsMan stopSearching];
|
[_mdnsMan stopSearching];
|
||||||
@@ -87,10 +95,18 @@
|
|||||||
|
|
||||||
- (void) stopDiscoveryBlocking {
|
- (void) stopDiscoveryBlocking {
|
||||||
Log(LOG_I, @"Stopping discovery and waiting for workers to stop");
|
Log(LOG_I, @"Stopping discovery and waiting for workers to stop");
|
||||||
shouldDiscover = NO;
|
|
||||||
[_mdnsMan stopSearching];
|
if (shouldDiscover) {
|
||||||
[_opQueue cancelAllOperations];
|
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];
|
[_opQueue waitUntilAllOperationsAreFinished];
|
||||||
|
|
||||||
Log(LOG_I, @"All discovery workers stopped");
|
Log(LOG_I, @"All discovery workers stopped");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,6 +117,12 @@
|
|||||||
|
|
||||||
TemporaryHost *existingHost = [self getHostInDiscovery:host.uuid];
|
TemporaryHost *existingHost = [self getHostInDiscovery:host.uuid];
|
||||||
if (existingHost != nil) {
|
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
|
// Update address of existing host
|
||||||
if (host.address != nil) {
|
if (host.address != nil) {
|
||||||
existingHost.address = host.address;
|
existingHost.address = host.address;
|
||||||
@@ -112,6 +134,7 @@
|
|||||||
existingHost.externalAddress = host.externalAddress;
|
existingHost.externalAddress = host.externalAddress;
|
||||||
}
|
}
|
||||||
existingHost.activeAddress = host.activeAddress;
|
existingHost.activeAddress = host.activeAddress;
|
||||||
|
existingHost.online = host.online;
|
||||||
return NO;
|
return NO;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -142,28 +165,23 @@
|
|||||||
[_hostQueue removeObject:host];
|
[_hostQueue removeObject:host];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Override from MDNSCallback
|
// Override from MDNSCallback - called in a worker thread
|
||||||
- (void)updateHosts:(NSArray *)hosts {
|
- (void)updateHost:(TemporaryHost*)host {
|
||||||
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
// Discover the hosts before adding to eliminate duplicates
|
||||||
// Discover the hosts before adding to eliminate duplicates
|
Log(LOG_D, @"Found host through MDNS: %@:", host.name);
|
||||||
for (TemporaryHost* host in hosts) {
|
// Since this is on a background thread, we do not need to use the opQueue
|
||||||
Log(LOG_I, @"Found host through MDNS: %@:", host.name);
|
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
|
||||||
// Since this is on a background thread, we do not need to use the opQueue
|
[worker discoverHost];
|
||||||
DiscoveryWorker* worker = (DiscoveryWorker*)[self createWorkerForHost:host];
|
if ([self addHostToDiscovery:host]) {
|
||||||
[worker discoverHost];
|
Log(LOG_I, @"Found new host through MDNS: %@:", host.name);
|
||||||
if ([self addHostToDiscovery:host]) {
|
[self->_callback updateAllHosts:self->_hostQueue];
|
||||||
Log(LOG_I, @"Adding host to discovery: %@", host.name);
|
} else {
|
||||||
[_callback updateAllHosts:_hostQueue];
|
Log(LOG_D, @"Found existing host through MDNS: %@", host.name);
|
||||||
} else {
|
}
|
||||||
Log(LOG_I, @"Not adding host to discovery: %@", host.name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
|
- (TemporaryHost*) getHostInDiscovery:(NSString*)uuidString {
|
||||||
for (int i = 0; i < _hostQueue.count; i++) {
|
for (TemporaryHost* discoveredHost in _hostQueue) {
|
||||||
TemporaryHost* discoveredHost = [_hostQueue objectAtIndex:i];
|
|
||||||
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
|
if (discoveredHost.uuid.length > 0 && [discoveredHost.uuid isEqualToString:uuidString]) {
|
||||||
return discoveredHost;
|
return discoveredHost;
|
||||||
}
|
}
|
||||||
@@ -172,7 +190,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSOperation*) createWorkerForHost:(TemporaryHost*)host {
|
- (NSOperation*) createWorkerForHost:(TemporaryHost*)host {
|
||||||
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId cert:_cert];
|
DiscoveryWorker* worker = [[DiscoveryWorker alloc] initWithHost:host uniqueId:_uniqueId];
|
||||||
return worker;
|
return worker;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,12 +6,11 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
|
|
||||||
@interface DiscoveryWorker : NSOperation
|
@interface DiscoveryWorker : NSOperation
|
||||||
|
|
||||||
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId cert:(NSData*)cert;
|
- (id) initWithHost:(TemporaryHost*)host uniqueId:(NSString*)uniqueId;
|
||||||
- (void) discoverHost;
|
- (void) discoverHost;
|
||||||
- (TemporaryHost*) getHost;
|
- (TemporaryHost*) getHost;
|
||||||
|
|
||||||
|
|||||||
@@ -16,16 +16,14 @@
|
|||||||
@implementation DiscoveryWorker {
|
@implementation DiscoveryWorker {
|
||||||
TemporaryHost* _host;
|
TemporaryHost* _host;
|
||||||
NSString* _uniqueId;
|
NSString* _uniqueId;
|
||||||
NSData* _cert;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
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];
|
self = [super init];
|
||||||
_host = host;
|
_host = host;
|
||||||
_uniqueId = uniqueId;
|
_uniqueId = uniqueId;
|
||||||
_cert = cert;
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +46,12 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
if (_host.localAddress != nil) {
|
if (_host.localAddress != nil) {
|
||||||
[array addObject:_host.localAddress];
|
[array addObject:_host.localAddress];
|
||||||
}
|
}
|
||||||
if (_host.externalAddress != nil) {
|
|
||||||
[array addObject:_host.externalAddress];
|
|
||||||
}
|
|
||||||
if (_host.address != nil) {
|
if (_host.address != nil) {
|
||||||
[array addObject:_host.address];
|
[array addObject:_host.address];
|
||||||
}
|
}
|
||||||
|
if (_host.externalAddress != nil) {
|
||||||
|
[array addObject:_host.externalAddress];
|
||||||
|
}
|
||||||
|
|
||||||
// Remove duplicate addresses from the list.
|
// Remove duplicate addresses from the list.
|
||||||
// This is done using an array rather than a set
|
// This is done using an array rather than a set
|
||||||
@@ -88,8 +86,8 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
|
|
||||||
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
|
Log(LOG_D, @"%@ has %d unique addresses", _host.name, [addresses count]);
|
||||||
|
|
||||||
// Give the PC 3 tries to respond before declaring it offline
|
// Give the PC 2 tries to respond before declaring it offline
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 2; i++) {
|
||||||
for (NSString *address in addresses) {
|
for (NSString *address in addresses) {
|
||||||
if (self.cancelled) {
|
if (self.cancelled) {
|
||||||
// Get out without updating the status because
|
// Get out without updating the status because
|
||||||
@@ -98,7 +96,7 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address];
|
ServerInfoResponse* serverInfoResp = [self requestInfoAtAddress:address cert:_host.serverCert];
|
||||||
receivedResponse = [self checkResponse:serverInfoResp];
|
receivedResponse = [self checkResponse:serverInfoResp];
|
||||||
if (receivedResponse) {
|
if (receivedResponse) {
|
||||||
[serverInfoResp populateHost:_host];
|
[serverInfoResp populateHost:_host];
|
||||||
@@ -112,13 +110,9 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (receivedResponse) {
|
if (receivedResponse) {
|
||||||
Log(LOG_I, @"Received serverinfo response on try %d", i);
|
Log(LOG_D, @"Received serverinfo response on try %d", i);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
// Wait for one second then retry
|
|
||||||
[NSThread sleepForTimeInterval:1];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_host.online = receivedResponse;
|
_host.online = receivedResponse;
|
||||||
@@ -127,14 +121,13 @@ static const float POLL_RATE = 2.0f; // Poll every 2 seconds
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address {
|
- (ServerInfoResponse*) requestInfoAtAddress:(NSString*)address cert:(NSData*)cert {
|
||||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
|
HttpManager* hMan = [[HttpManager alloc] initWithHost:address
|
||||||
uniqueId:_uniqueId
|
uniqueId:_uniqueId
|
||||||
deviceName:deviceName
|
serverCert:cert];
|
||||||
cert:_cert];
|
|
||||||
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
|
ServerInfoResponse* response = [[ServerInfoResponse alloc] init];
|
||||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:response
|
||||||
withUrlRequest:[hMan newServerInfoRequest]
|
withUrlRequest:[hMan newServerInfoRequest:true]
|
||||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,24 +6,26 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "HttpResponse.h"
|
#import "HttpResponse.h"
|
||||||
#import "HttpRequest.h"
|
#import "HttpRequest.h"
|
||||||
|
#import "StreamConfiguration.h"
|
||||||
|
|
||||||
@interface HttpManager : NSObject <NSURLSessionDelegate>
|
@interface HttpManager : NSObject <NSURLSessionDelegate>
|
||||||
|
|
||||||
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId deviceName:(NSString*) deviceName cert:(NSData*) cert;
|
- (id) initWithHost:(NSString*) host uniqueId:(NSString*) uniqueId serverCert:(NSData*) serverCert;
|
||||||
- (NSURLRequest*) newPairRequest:(NSData*)salt;
|
- (void) setServerCert:(NSData*) serverCert;
|
||||||
|
- (NSURLRequest*) newPairRequest:(NSData*)salt clientCert:(NSData*)clientCert;
|
||||||
- (NSURLRequest*) newUnpairRequest;
|
- (NSURLRequest*) newUnpairRequest;
|
||||||
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge;
|
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge;
|
||||||
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp;
|
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp;
|
||||||
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret;
|
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret;
|
||||||
- (NSURLRequest*) newPairChallenge;
|
- (NSURLRequest*) newPairChallenge;
|
||||||
- (NSURLRequest*) newAppListRequest;
|
- (NSURLRequest*) newAppListRequest;
|
||||||
- (NSURLRequest*) newServerInfoRequest;
|
- (NSURLRequest*) newServerInfoRequest:(bool)fastFail;
|
||||||
|
- (NSURLRequest*) newHttpServerInfoRequest:(bool)fastFail;
|
||||||
- (NSURLRequest*) newHttpServerInfoRequest;
|
- (NSURLRequest*) newHttpServerInfoRequest;
|
||||||
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid;
|
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config;
|
||||||
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId;
|
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config;
|
||||||
- (NSURLRequest*) newQuitAppRequest;
|
- (NSURLRequest*) newQuitAppRequest;
|
||||||
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
|
||||||
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
- (void) executeRequestSynchronously:(HttpRequest*)request;
|
||||||
|
|||||||
+138
-52
@@ -14,19 +14,23 @@
|
|||||||
#include <libxml2/libxml/xmlreader.h>
|
#include <libxml2/libxml/xmlreader.h>
|
||||||
#include <string.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 {
|
@implementation HttpManager {
|
||||||
NSURLSession* _urlSession;
|
NSURLSession* _urlSession;
|
||||||
NSString* _baseHTTPURL;
|
NSString* _baseHTTPURL;
|
||||||
NSString* _baseHTTPSURL;
|
NSString* _baseHTTPSURL;
|
||||||
NSString* _host;
|
|
||||||
NSString* _uniqueId;
|
NSString* _uniqueId;
|
||||||
NSString* _deviceName;
|
NSString* _deviceName;
|
||||||
NSData* _cert;
|
NSData* _serverCert;
|
||||||
NSMutableData* _respData;
|
NSMutableData* _respData;
|
||||||
NSData* _requestResp;
|
NSData* _requestResp;
|
||||||
dispatch_semaphore_t _requestLock;
|
dispatch_semaphore_t _requestLock;
|
||||||
|
|
||||||
BOOL _errorOccurred;
|
NSError* _error;
|
||||||
}
|
}
|
||||||
|
|
||||||
static const NSString* HTTP_PORT = @"47989";
|
static const NSString* HTTP_PORT = @"47989";
|
||||||
@@ -39,49 +43,66 @@ static const NSString* HTTPS_PORT = @"47984";
|
|||||||
return [xmlString dataUsingEncoding:NSUTF8StringEncoding];
|
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];
|
self = [super init];
|
||||||
_host = host;
|
// Use the same UID for all Moonlight clients to allow them
|
||||||
_uniqueId = uniqueId;
|
// quit games started on another Moonlight client.
|
||||||
|
_uniqueId = @"0123456789ABCDEF";
|
||||||
_deviceName = deviceName;
|
_deviceName = deviceName;
|
||||||
_cert = cert;
|
_serverCert = serverCert;
|
||||||
_baseHTTPURL = [NSString stringWithFormat:@"http://%@:%@", host, HTTP_PORT];
|
|
||||||
_baseHTTPSURL = [NSString stringWithFormat:@"https://%@:%@", host, HTTPS_PORT];
|
|
||||||
_requestLock = dispatch_semaphore_create(0);
|
_requestLock = dispatch_semaphore_create(0);
|
||||||
_respData = [[NSMutableData alloc] init];
|
_respData = [[NSMutableData alloc] init];
|
||||||
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
NSURLSessionConfiguration* config = [NSURLSessionConfiguration ephemeralSessionConfiguration];
|
||||||
_urlSession = [NSURLSession sessionWithConfiguration:config delegate:self delegateQueue:nil];
|
_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;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) executeRequestSynchronously:(HttpRequest*)request {
|
- (void) executeRequestSynchronously:(HttpRequest*)request {
|
||||||
Log(LOG_D, @"Making Request: %@", request);
|
|
||||||
[_respData setLength:0];
|
[_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) {
|
[[_urlSession dataTaskWithRequest:request.request completionHandler:^(NSData * __nullable data, NSURLResponse * __nullable response, NSError * __nullable error) {
|
||||||
|
|
||||||
if (error != NULL) {
|
if (error != NULL) {
|
||||||
Log(LOG_D, @"Connection error: %@", error);
|
Log(LOG_D, @"Connection error: %@", error);
|
||||||
_errorOccurred = true;
|
self->_error = error;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
Log(LOG_D, @"Received response: %@", response);
|
Log(LOG_D, @"Received response: %@", response);
|
||||||
|
|
||||||
if (data != NULL) {
|
if (data != NULL) {
|
||||||
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
|
Log(LOG_D, @"\n\nReceived data: %@\n\n", [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]);
|
||||||
[_respData appendData:data];
|
[self->_respData appendData:data];
|
||||||
if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) {
|
if ([[NSString alloc] initWithData:self->_respData encoding:NSUTF8StringEncoding] != nil) {
|
||||||
_requestResp = [HttpManager fixXmlVersion:_respData];
|
self->_requestResp = [HttpManager fixXmlVersion:self->_respData];
|
||||||
} else {
|
} else {
|
||||||
_requestResp = _respData;
|
self->_requestResp = self->_respData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch_semaphore_signal(_requestLock);
|
dispatch_semaphore_signal(self->_requestLock);
|
||||||
}] resume];
|
}] resume];
|
||||||
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
|
dispatch_semaphore_wait(_requestLock, DISPATCH_TIME_FOREVER);
|
||||||
|
|
||||||
if (!_errorOccurred && request.response) {
|
if (!_error && request.response) {
|
||||||
[request.response populateWithData:_requestResp];
|
[request.response populateWithData:_requestResp];
|
||||||
|
|
||||||
// If the fallback error code was detected, issue the fallback request
|
// If the fallback error code was detected, issue the fallback request
|
||||||
@@ -93,92 +114,127 @@ static const NSString* HTTPS_PORT = @"47984";
|
|||||||
[self executeRequestSynchronously:request];
|
[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];
|
NSURL* url = [[NSURL alloc] initWithString:urlString];
|
||||||
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
NSMutableURLRequest* request = [NSMutableURLRequest requestWithURL:url];
|
||||||
if (normalTimeout) {
|
[request setTimeoutInterval:timeout];
|
||||||
// Timeout the request after 7 seconds
|
|
||||||
[request setTimeoutInterval:7];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Timeout the request after 60 seconds
|
|
||||||
[request setTimeoutInterval:60];
|
|
||||||
}
|
|
||||||
return request;
|
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=%@",
|
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
|
// 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 {
|
- (NSURLRequest*) newUnpairRequest {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
NSString* urlString = [NSString stringWithFormat:@"%@/unpair?uniqueid=%@", _baseHTTPURL, _uniqueId];
|
||||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
|
- (NSURLRequest*) newChallengeRequest:(NSData*)challenge {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
|
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientchallenge=%@",
|
||||||
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
|
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challenge]];
|
||||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp {
|
- (NSURLRequest*) newChallengeRespRequest:(NSData*)challengeResp {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&serverchallengeresp=%@",
|
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&serverchallengeresp=%@",
|
||||||
_baseHTTPSURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
|
_baseHTTPURL, _uniqueId, _deviceName, [self bytesToHex:challengeResp]];
|
||||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret {
|
- (NSURLRequest*) newClientSecretRespRequest:(NSString*)clientPairSecret {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPSURL, _uniqueId, _deviceName, clientPairSecret];
|
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&clientpairingsecret=%@", _baseHTTPURL, _uniqueId, _deviceName, clientPairSecret];
|
||||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
return [self createRequestFromString:urlString timeout:NORMAL_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newPairChallenge {
|
- (NSURLRequest*) newPairChallenge {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/pair?uniqueid=%@&devicename=%@&updateState=1&phrase=pairchallenge", _baseHTTPSURL, _uniqueId, _deviceName];
|
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 {
|
- (NSURLRequest *)newAppListRequest {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/applist?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
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];
|
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 {
|
- (NSURLRequest *)newHttpServerInfoRequest {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/serverinfo", _baseHTTPURL];
|
return [self newHttpServerInfoRequest:false];
|
||||||
return [self createRequestFromString:urlString enableTimeout:TRUE];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid {
|
- (NSURLRequest*) newLaunchRequest:(StreamConfiguration*)config {
|
||||||
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];
|
// 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
|
// 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 {
|
- (NSURLRequest*) newResumeRequest:(StreamConfiguration*)config {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/resume?uniqueid=%@&rikey=%@&rikeyid=%d", _baseHTTPSURL, _uniqueId, riKey, riKeyId];
|
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
|
// This blocks while the app is resuming
|
||||||
return [self createRequestFromString:urlString enableTimeout:FALSE];
|
return [self createRequestFromString:urlString timeout:LONG_TIMEOUT_SEC];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSURLRequest*) newQuitAppRequest {
|
- (NSURLRequest*) newQuitAppRequest {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/cancel?uniqueid=%@", _baseHTTPSURL, _uniqueId];
|
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 {
|
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
|
||||||
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseHTTPSURL, _uniqueId, 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 {
|
- (NSString*) bytesToHex:(NSData*)data {
|
||||||
@@ -229,6 +285,36 @@ static const NSString* HTTPS_PORT = @"47984";
|
|||||||
// Allow untrusted server certificates
|
// Allow untrusted server certificates
|
||||||
if([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
|
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,
|
completionHandler(NSURLSessionAuthChallengeUseCredential,
|
||||||
[NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]);
|
[NSURLCredential credentialForTrust: challenge.protectionSpace.serverTrust]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "HttpResponse.h"
|
#import "HttpResponse.h"
|
||||||
|
|
||||||
@interface HttpRequest : NSObject
|
@interface HttpRequest : NSObject
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
static NSString* TAG_STATUS_CODE = @"status_code";
|
static NSString* TAG_STATUS_CODE = @"status_code";
|
||||||
static NSString* TAG_STATUS_MESSAGE = @"status_message";
|
static NSString* TAG_STATUS_MESSAGE = @"status_message";
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
xmlFreeDoc(docPtr);
|
xmlFreeDoc(docPtr);
|
||||||
|
|
||||||
|
Log(LOG_D, @"Parsed XML data: %@", _elements);
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -6,11 +6,11 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
#import "TemporaryHost.h"
|
||||||
|
|
||||||
@protocol MDNSCallback <NSObject>
|
@protocol MDNSCallback <NSObject>
|
||||||
|
|
||||||
- (void) updateHosts:(NSArray*)hosts;
|
- (void) updateHost:(TemporaryHost*)host;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -9,11 +9,15 @@
|
|||||||
#import "MDNSManager.h"
|
#import "MDNSManager.h"
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
|
|
||||||
|
#include <arpa/inet.h>
|
||||||
|
|
||||||
|
#include <Limelight.h>
|
||||||
|
|
||||||
@implementation MDNSManager {
|
@implementation MDNSManager {
|
||||||
NSNetServiceBrowser* mDNSBrowser;
|
NSNetServiceBrowser* mDNSBrowser;
|
||||||
NSMutableArray* domains;
|
|
||||||
NSMutableArray* services;
|
NSMutableArray* services;
|
||||||
BOOL scanActive;
|
BOOL scanActive;
|
||||||
|
BOOL timerPending;
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
||||||
@@ -23,43 +27,65 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
|||||||
|
|
||||||
self.callback = callback;
|
self.callback = callback;
|
||||||
|
|
||||||
|
scanActive = FALSE;
|
||||||
|
|
||||||
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
|
mDNSBrowser = [[NSNetServiceBrowser alloc] init];
|
||||||
[mDNSBrowser setDelegate:self];
|
[mDNSBrowser setDelegate:self];
|
||||||
|
|
||||||
domains = [[NSMutableArray alloc] init];
|
|
||||||
services = [[NSMutableArray alloc] init];
|
services = [[NSMutableArray alloc] init];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) searchForHosts {
|
- (void) searchForHosts {
|
||||||
|
if (scanActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log(LOG_I, @"Starting mDNS discovery");
|
Log(LOG_I, @"Starting mDNS discovery");
|
||||||
scanActive = TRUE;
|
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 {
|
- (void) stopSearching {
|
||||||
|
if (!scanActive) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
Log(LOG_I, @"Stopping mDNS discovery");
|
Log(LOG_I, @"Stopping mDNS discovery");
|
||||||
scanActive = FALSE;
|
scanActive = FALSE;
|
||||||
[mDNSBrowser stop];
|
[mDNSBrowser stop];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSArray*) getFoundHosts {
|
|
||||||
NSMutableArray* hosts = [[NSMutableArray alloc] init];
|
|
||||||
for (NSNetService* service in services) {
|
|
||||||
if (service.hostName != nil) {
|
|
||||||
TemporaryHost* host = [[TemporaryHost alloc] init];
|
|
||||||
host.activeAddress = host.localAddress = service.hostName;
|
|
||||||
host.name = service.hostName;
|
|
||||||
[hosts addObject:host];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return hosts;
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)netServiceDidResolveAddress:(NSNetService *)service {
|
- (void)netServiceDidResolveAddress:(NSNetService *)service {
|
||||||
Log(LOG_I, @"Resolved address: %@ -> %@", service, service.hostName);
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
||||||
[self.callback updateHosts:[self getFoundHosts]];
|
Log(LOG_D, @"Resolved address: %@ -> %@", service, service.hostName);
|
||||||
|
TemporaryHost* host = [[TemporaryHost alloc] init];
|
||||||
|
|
||||||
|
// Since we discovered this host over mDNS, we know we're on the same network
|
||||||
|
// as the PC and we can use our current WAN address as a likely candidate
|
||||||
|
// for our PC's external address.
|
||||||
|
struct in_addr wanAddr;
|
||||||
|
int err = LiFindExternalAddressIP4("stun.moonlight-stream.org", 3478, &wanAddr.s_addr);
|
||||||
|
if (err == 0) {
|
||||||
|
char addrStr[INET_ADDRSTRLEN];
|
||||||
|
inet_ntop(AF_INET, &wanAddr, addrStr, sizeof(addrStr));
|
||||||
|
host.externalAddress = [NSString stringWithFormat: @"%s", addrStr];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log(LOG_E, @"STUN failed to get WAN address: %d", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
host.activeAddress = host.localAddress = service.hostName;
|
||||||
|
host.name = service.hostName;
|
||||||
|
[self.callback updateHost:host];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
|
- (void)netService:(NSNetService *)sender didNotResolve:(NSDictionary *)errorDict {
|
||||||
@@ -74,12 +100,14 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didFindService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||||
Log(LOG_I, @"Found service: %@", aNetService);
|
Log(LOG_D, @"Found service: %@", aNetService);
|
||||||
[aNetService setDelegate:self];
|
|
||||||
[aNetService resolveWithTimeout:5];
|
|
||||||
|
|
||||||
[services removeObject:aNetService];
|
if (![services containsObject:aNetService]) {
|
||||||
[services addObject: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 {
|
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didRemoveService:(NSNetService *)aNetService moreComing:(BOOL)moreComing {
|
||||||
@@ -90,23 +118,29 @@ static NSString* NV_SERVICE_TYPE = @"_nvstream._tcp";
|
|||||||
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
|
- (void)netServiceBrowser:(NSNetServiceBrowser *)aNetServiceBrowser didNotSearch:(NSDictionary *)errorDict {
|
||||||
Log(LOG_W, @"Did not perform search: \n%@", [errorDict description]);
|
Log(LOG_W, @"Did not perform search: \n%@", [errorDict description]);
|
||||||
|
|
||||||
// Schedule a retry in 2 seconds
|
// We'll schedule a retry in startSearchTimerCallback
|
||||||
[NSTimer scheduledTimerWithTimeInterval:2.0
|
|
||||||
target:self
|
|
||||||
selector:@selector(retrySearchTimerCallback:)
|
|
||||||
userInfo:nil
|
|
||||||
repeats:NO];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)retrySearchTimerCallback:(NSTimer *)timer {
|
- (void)startSearchTimerCallback:(NSTimer *)timer {
|
||||||
// Check if we've been stopped since this was queued
|
// Check if we've been stopped since this was queued
|
||||||
if (!scanActive) {
|
if (!scanActive) {
|
||||||
|
timerPending = FALSE;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log(LOG_I, @"Retrying mDNS search");
|
Log(LOG_D, @"Restarting mDNS search");
|
||||||
[mDNSBrowser stop];
|
[mDNSBrowser stop];
|
||||||
[mDNSBrowser searchForServicesOfType:NV_SERVICE_TYPE inDomain:@""];
|
[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 {
|
- (void)retryResolveTimerCallback:(NSTimer *)timer {
|
||||||
|
|||||||
@@ -6,20 +6,19 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "HttpManager.h"
|
#import "HttpManager.h"
|
||||||
|
|
||||||
@protocol PairCallback <NSObject>
|
@protocol PairCallback <NSObject>
|
||||||
|
|
||||||
- (void) showPIN:(NSString*)PIN;
|
- (void) showPIN:(NSString*)PIN;
|
||||||
- (void) pairSuccessful;
|
- (void) pairSuccessful:(NSData*)serverCert;
|
||||||
- (void) pairFailed:(NSString*)message;
|
- (void) pairFailed:(NSString*)message;
|
||||||
- (void) alreadyPaired;
|
- (void) alreadyPaired;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@interface PairManager : NSOperation
|
@interface PairManager : NSOperation
|
||||||
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback;
|
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback;
|
||||||
- (NSString*) generatePIN;
|
- (NSString*) generatePIN;
|
||||||
- (NSData*) saltPIN:(NSString*)PIN;
|
- (NSData*) saltPIN:(NSString*)PIN;
|
||||||
- (void) initiatePair:(int)serverMajorVersion;
|
- (void) initiatePair:(int)serverMajorVersion;
|
||||||
|
|||||||
@@ -17,28 +17,28 @@
|
|||||||
|
|
||||||
@implementation PairManager {
|
@implementation PairManager {
|
||||||
HttpManager* _httpManager;
|
HttpManager* _httpManager;
|
||||||
NSData* _cert;
|
NSData* _clientCert;
|
||||||
id<PairCallback> _callback;
|
id<PairCallback> _callback;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithManager:(HttpManager*)httpManager andCert:(NSData*)cert callback:(id<PairCallback>)callback {
|
- (id) initWithManager:(HttpManager*)httpManager clientCert:(NSData*)clientCert callback:(id<PairCallback>)callback {
|
||||||
self = [super init];
|
self = [super init];
|
||||||
_httpManager = httpManager;
|
_httpManager = httpManager;
|
||||||
_cert = cert;
|
_clientCert = clientCert;
|
||||||
_callback = callback;
|
_callback = callback;
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) main {
|
- (void) main {
|
||||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest]
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[_httpManager newServerInfoRequest:false]
|
||||||
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
|
fallbackError:401 fallbackRequest:[_httpManager newHttpServerInfoRequest]]];
|
||||||
if (serverInfoResp == nil) {
|
if (serverInfoResp == nil) {
|
||||||
[_callback pairFailed:@"Unable to connect to PC"];
|
[_callback pairFailed:@"Unable to connect to PC"];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ([serverInfoResp isStatusOk]) {
|
if ([serverInfoResp isStatusOk]) {
|
||||||
if (![[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_AVAILABLE"]) {
|
if ([[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
|
||||||
[_callback pairFailed:@"You must stop streaming before attempting to pair."];
|
[_callback pairFailed:@"You must stop streaming before attempting to pair."];
|
||||||
} else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
|
} else if (![[serverInfoResp getStringTag:@"PairStatus"] isEqual:@"1"]) {
|
||||||
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
||||||
@@ -53,6 +53,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All codepaths must call pairFailed or pairSuccessful exactly once before returning!
|
||||||
- (void) initiatePair:(int)serverMajorVersion {
|
- (void) initiatePair:(int)serverMajorVersion {
|
||||||
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
|
Log(LOG_I, @"Pairing with generation %d server", serverMajorVersion);
|
||||||
|
|
||||||
@@ -62,8 +63,9 @@
|
|||||||
[_callback showPIN:PIN];
|
[_callback showPIN:PIN];
|
||||||
|
|
||||||
HttpResponse* pairResp = [[HttpResponse alloc] init];
|
HttpResponse* pairResp = [[HttpResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:pairResp withUrlRequest:[_httpManager newPairRequest:salt clientCert:_clientCert]]];
|
||||||
if (![self verifyResponseStatus:pairResp]) {
|
if (![self verifyResponseStatus:pairResp]) {
|
||||||
|
[_callback pairFailed:@"Pairing stage #1 failed"];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
NSInteger pairedStatus;
|
NSInteger pairedStatus;
|
||||||
@@ -78,6 +80,10 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Pin the cert for TLS usage on this host
|
||||||
|
NSData* derCertBytes = [CryptoManager pemToDer:[Utils hexToBytes:plainCert]];
|
||||||
|
[_httpManager setServerCert:derCertBytes];
|
||||||
|
|
||||||
CryptoManager* cryptoMan = [[CryptoManager alloc] init];
|
CryptoManager* cryptoMan = [[CryptoManager alloc] init];
|
||||||
NSData* aesKey;
|
NSData* aesKey;
|
||||||
|
|
||||||
@@ -97,10 +103,9 @@
|
|||||||
|
|
||||||
HttpResponse* challengeResp = [[HttpResponse alloc] init];
|
HttpResponse* challengeResp = [[HttpResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:challengeResp withUrlRequest:[_httpManager newChallengeRequest:encryptedChallenge]]];
|
||||||
if (![self verifyResponseStatus:challengeResp]) {
|
if (![self verifyResponseStatus:challengeResp] ||
|
||||||
return;
|
![challengeResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||||
}
|
pairedStatus != 1) {
|
||||||
if (![challengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
|
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||||
[_callback pairFailed:@"Pairing stage #2 failed"];
|
[_callback pairFailed:@"Pairing stage #2 failed"];
|
||||||
return;
|
return;
|
||||||
@@ -113,7 +118,7 @@
|
|||||||
NSData* serverChallenge = [decServerChallengeResp subdataWithRange:NSMakeRange(hashLength, 16)];
|
NSData* serverChallenge = [decServerChallengeResp subdataWithRange:NSMakeRange(hashLength, 16)];
|
||||||
|
|
||||||
NSData* clientSecret = [Utils randomBytes:16];
|
NSData* clientSecret = [Utils randomBytes:16];
|
||||||
NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_cert]] with:clientSecret];
|
NSData* challengeRespHashInput = [self concatData:[self concatData:serverChallenge with:[CryptoManager getSignatureFromCert:_clientCert]] with:clientSecret];
|
||||||
NSData* challengeRespHash;
|
NSData* challengeRespHash;
|
||||||
if (serverMajorVersion >= 7) {
|
if (serverMajorVersion >= 7) {
|
||||||
challengeRespHash = [cryptoMan SHA256HashData: challengeRespHashInput];
|
challengeRespHash = [cryptoMan SHA256HashData: challengeRespHashInput];
|
||||||
@@ -125,10 +130,9 @@
|
|||||||
|
|
||||||
HttpResponse* secretResp = [[HttpResponse alloc] init];
|
HttpResponse* secretResp = [[HttpResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:secretResp withUrlRequest:[_httpManager newChallengeRespRequest:challengeRespEncrypted]]];
|
||||||
if (![self verifyResponseStatus:secretResp]) {
|
if (![self verifyResponseStatus:secretResp] ||
|
||||||
return;
|
![secretResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||||
}
|
pairedStatus != 1) {
|
||||||
if (![secretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
|
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||||
[_callback pairFailed:@"Pairing stage #3 failed"];
|
[_callback pairFailed:@"Pairing stage #3 failed"];
|
||||||
return;
|
return;
|
||||||
@@ -161,10 +165,9 @@
|
|||||||
NSData* clientPairingSecret = [self concatData:clientSecret with:[cryptoMan signData:clientSecret withKey:[CryptoManager readKeyFromFile]]];
|
NSData* clientPairingSecret = [self concatData:clientSecret with:[cryptoMan signData:clientSecret withKey:[CryptoManager readKeyFromFile]]];
|
||||||
HttpResponse* clientSecretResp = [[HttpResponse alloc] init];
|
HttpResponse* clientSecretResp = [[HttpResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientSecretResp withUrlRequest:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientSecretResp withUrlRequest:[_httpManager newClientSecretRespRequest:[Utils bytesToHex:clientPairingSecret]]]];
|
||||||
if (![self verifyResponseStatus:clientSecretResp]) {
|
if (![self verifyResponseStatus:clientSecretResp] ||
|
||||||
return;
|
![clientSecretResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||||
}
|
pairedStatus != 1) {
|
||||||
if (![clientSecretResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
|
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||||
[_callback pairFailed:@"Pairing stage #4 failed"];
|
[_callback pairFailed:@"Pairing stage #4 failed"];
|
||||||
return;
|
return;
|
||||||
@@ -172,15 +175,15 @@
|
|||||||
|
|
||||||
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
|
HttpResponse* clientPairChallengeResp = [[HttpResponse alloc] init];
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestForResponse:clientPairChallengeResp withUrlRequest:[_httpManager newPairChallenge]]];
|
||||||
if (![self verifyResponseStatus:clientPairChallengeResp]) {
|
if (![self verifyResponseStatus:clientPairChallengeResp] ||
|
||||||
return;
|
![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] ||
|
||||||
}
|
pairedStatus != 1) {
|
||||||
if (![clientPairChallengeResp getIntTag:@"paired" value:&pairedStatus] || pairedStatus != 1) {
|
|
||||||
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
[_httpManager executeRequestSynchronously:[HttpRequest requestWithUrlRequest:[_httpManager newUnpairRequest]]];
|
||||||
[_callback pairFailed:@"Pairing stage #5 failed"];
|
[_callback pairFailed:@"Pairing stage #5 failed"];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
[_callback pairSuccessful];
|
|
||||||
|
[_callback pairSuccessful: derCertBytes];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
|
- (BOOL) verifyResponseStatus:(HttpResponse*)resp {
|
||||||
|
|||||||
@@ -19,14 +19,20 @@
|
|||||||
|
|
||||||
- (void) populateHost:(TemporaryHost*)host {
|
- (void) populateHost:(TemporaryHost*)host {
|
||||||
host.name = [[self getStringTag:TAG_HOSTNAME] trim];
|
host.name = [[self getStringTag:TAG_HOSTNAME] trim];
|
||||||
host.externalAddress = [[self getStringTag:TAG_EXTERNAL_IP] trim];
|
|
||||||
host.localAddress = [[self getStringTag:TAG_LOCAL_IP] trim];
|
host.localAddress = [[self getStringTag:TAG_LOCAL_IP] trim];
|
||||||
host.uuid = [[self getStringTag:TAG_UNIQUE_ID] trim];
|
host.uuid = [[self getStringTag:TAG_UNIQUE_ID] trim];
|
||||||
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
|
host.mac = [[self getStringTag:TAG_MAC_ADDRESS] trim];
|
||||||
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
|
host.currentGame = [[self getStringTag:TAG_CURRENT_GAME] trim];
|
||||||
|
|
||||||
|
// Modern GFE versions don't actually give us a WAN address anymore
|
||||||
|
// so we leave the one that we populated from mDNS discovery via STUN.
|
||||||
|
NSString *wanAddr = [[self getStringTag:TAG_EXTERNAL_IP] trim];
|
||||||
|
if (wanAddr) {
|
||||||
|
host.externalAddress = wanAddr;
|
||||||
|
}
|
||||||
|
|
||||||
NSString *state = [[self getStringTag:TAG_STATE] trim];
|
NSString *state = [[self getStringTag:TAG_STATE] trim];
|
||||||
if ([state hasSuffix:@"_SERVER_AVAILABLE"]) {
|
if (![state hasSuffix:@"_SERVER_BUSY"]) {
|
||||||
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
// GFE 2.8 started keeping currentgame set to the last game played. As a result, it no longer
|
||||||
// has the semantics that its name would indicate. To contain the effects of this change as much
|
// has the semantics that its name would indicate. To contain the effects of this change as much
|
||||||
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
// as possible, we'll force the current game to zero if the server isn't in a streaming session.
|
||||||
@@ -39,6 +45,11 @@
|
|||||||
} else {
|
} else {
|
||||||
host.pairState = PairStateUnknown;
|
host.pairState = PairStateUnknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
NSString *serverCodecModeString = [self getStringTag:@"ServerCodecModeSupport"];
|
||||||
|
if (serverCodecModeString != nil) {
|
||||||
|
host.serverCodecModeSupport = [[serverCodecModeString trim] intValue];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "TemporaryHost.h"
|
#import "TemporaryHost.h"
|
||||||
|
|
||||||
@interface WakeOnLanManager : NSObject
|
@interface WakeOnLanManager : NSObject
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
|
|||||||
+ (void) wakeHost:(TemporaryHost*)host {
|
+ (void) wakeHost:(TemporaryHost*)host {
|
||||||
NSData* wolPayload = [WakeOnLanManager createPayload:host];
|
NSData* wolPayload = [WakeOnLanManager createPayload:host];
|
||||||
|
|
||||||
for (int i = 0; i < 3; i++) {
|
for (int i = 0; i < 4; i++) {
|
||||||
const char* address;
|
const char* address;
|
||||||
struct addrinfo hints, *res, *curr;
|
struct addrinfo hints, *res, *curr;
|
||||||
|
|
||||||
@@ -44,6 +44,8 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
|
|||||||
address = [host.externalAddress UTF8String];
|
address = [host.externalAddress UTF8String];
|
||||||
} else if (i == 2 && host.address != nil) {
|
} else if (i == 2 && host.address != nil) {
|
||||||
address = [host.address UTF8String];
|
address = [host.address UTF8String];
|
||||||
|
} else if (i == 3) {
|
||||||
|
address = "255.255.255.255";
|
||||||
} else {
|
} else {
|
||||||
// Requested address wasn't present
|
// Requested address wasn't present
|
||||||
continue;
|
continue;
|
||||||
@@ -63,6 +65,7 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
|
|||||||
// may be different address families.
|
// may be different address families.
|
||||||
for (curr = res; curr != NULL; curr = curr->ai_next) {
|
for (curr = res; curr != NULL; curr = curr->ai_next) {
|
||||||
int wolSocket;
|
int wolSocket;
|
||||||
|
int val;
|
||||||
|
|
||||||
wolSocket = socket(curr->ai_family, SOCK_DGRAM, IPPROTO_UDP);
|
wolSocket = socket(curr->ai_family, SOCK_DGRAM, IPPROTO_UDP);
|
||||||
if (wolSocket < 0) {
|
if (wolSocket < 0) {
|
||||||
@@ -70,6 +73,9 @@ static const int ports[numPorts] = {7, 9, 47998, 47999, 48000};
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val = 1;
|
||||||
|
setsockopt(wolSocket, SOL_SOCKET, SO_BROADCAST, &val, sizeof(val));
|
||||||
|
|
||||||
struct sockaddr_storage addr;
|
struct sockaddr_storage addr;
|
||||||
memset(&addr, 0, sizeof(addr));
|
memset(&addr, 0, sizeof(addr));
|
||||||
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
|
memcpy(&addr, curr->ai_addr, curr->ai_addrlen);
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
//
|
||||||
|
// OSPortabilityDefs.h
|
||||||
|
// Moonlight
|
||||||
|
//
|
||||||
|
// Created by Cameron Gutman on 3/26/18.
|
||||||
|
// Copyright © 2018 Moonlight Stream. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
#ifndef OSPortabilityDefs_h
|
||||||
|
#define OSPortabilityDefs_h
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
#define OSImage UIImage
|
||||||
|
#define OSColor UIColor
|
||||||
|
#define OSView UIView
|
||||||
|
#define OSApplication UIApplication
|
||||||
|
#else
|
||||||
|
#define OSImage NSImage
|
||||||
|
#define OSColor NSColor
|
||||||
|
#define OSView NSView
|
||||||
|
#define OSApplication NSApplication
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#endif /* OSPortabilityDefs_h */
|
||||||
@@ -6,7 +6,6 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "VideoDecoderRenderer.h"
|
#import "VideoDecoderRenderer.h"
|
||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
|
|
||||||
@@ -18,8 +17,7 @@
|
|||||||
- (void) stageComplete:(const char*)stageName;
|
- (void) stageComplete:(const char*)stageName;
|
||||||
- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
|
- (void) stageFailed:(const char*)stageName withError:(long)errorCode;
|
||||||
- (void) launchFailed:(NSString*)message;
|
- (void) launchFailed:(NSString*)message;
|
||||||
- (void) displayMessage:(const char*)message;
|
- (void) rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor;
|
||||||
- (void) displayTransientMessage:(const char*)message;
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
+181
-220
@@ -13,7 +13,7 @@
|
|||||||
#import <VideoToolbox/VideoToolbox.h>
|
#import <VideoToolbox/VideoToolbox.h>
|
||||||
|
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
#include "opus.h"
|
#include "opus_multistream.h"
|
||||||
|
|
||||||
@implementation Connection {
|
@implementation Connection {
|
||||||
SERVER_INFORMATION _serverInfo;
|
SERVER_INFORMATION _serverInfo;
|
||||||
@@ -21,29 +21,31 @@
|
|||||||
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
|
CONNECTION_LISTENER_CALLBACKS _clCallbacks;
|
||||||
DECODER_RENDERER_CALLBACKS _drCallbacks;
|
DECODER_RENDERER_CALLBACKS _drCallbacks;
|
||||||
AUDIO_RENDERER_CALLBACKS _arCallbacks;
|
AUDIO_RENDERER_CALLBACKS _arCallbacks;
|
||||||
|
char _hostString[256];
|
||||||
|
char _appVersionString[32];
|
||||||
|
char _gfeVersionString[32];
|
||||||
}
|
}
|
||||||
|
|
||||||
static NSLock* initLock;
|
static NSLock* initLock;
|
||||||
static OpusDecoder *opusDecoder;
|
static OpusMSDecoder* opusDecoder;
|
||||||
static id<ConnectionCallbacks> _callbacks;
|
static id<ConnectionCallbacks> _callbacks;
|
||||||
|
|
||||||
#define PCM_BUFFER_SIZE 1024
|
|
||||||
#define OUTPUT_BUS 0
|
#define OUTPUT_BUS 0
|
||||||
|
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY {
|
#define MAX_CHANNEL_COUNT 2
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *next;
|
#define FRAME_SIZE 240
|
||||||
int length;
|
|
||||||
int offset;
|
|
||||||
char data[0];
|
|
||||||
};
|
|
||||||
|
|
||||||
#define MAX_QUEUE_ENTRIES 10
|
#define CIRCULAR_BUFFER_SIZE 32
|
||||||
|
|
||||||
static short decodedPcmBuffer[512];
|
static int audioBufferWriteIndex;
|
||||||
static NSLock *audioLock;
|
static int audioBufferReadIndex;
|
||||||
static struct AUDIO_BUFFER_QUEUE_ENTRY *audioBufferQueue;
|
static int activeChannelCount;
|
||||||
static int audioBufferQueueLength;
|
static short audioCircularBuffer[CIRCULAR_BUFFER_SIZE][FRAME_SIZE * MAX_CHANNEL_COUNT];
|
||||||
static AudioComponentInstance audioUnit;
|
|
||||||
|
#define AUDIO_QUEUE_BUFFERS 4
|
||||||
|
|
||||||
|
static AudioQueueRef audioQueue;
|
||||||
|
static AudioQueueBufferRef audioBuffers[AUDIO_QUEUE_BUFFERS];
|
||||||
static VideoDecoderRenderer* renderer;
|
static VideoDecoderRenderer* renderer;
|
||||||
|
|
||||||
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void* context, int drFlags)
|
||||||
@@ -55,6 +57,7 @@ int DrDecoderSetup(int videoFormat, int width, int height, int redrawRate, void*
|
|||||||
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
||||||
{
|
{
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
|
int ret;
|
||||||
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
|
unsigned char* data = (unsigned char*) malloc(decodeUnit->fullLength);
|
||||||
if (data == NULL) {
|
if (data == NULL) {
|
||||||
// A frame was lost due to OOM condition
|
// A frame was lost due to OOM condition
|
||||||
@@ -63,28 +66,47 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit)
|
|||||||
|
|
||||||
PLENTRY entry = decodeUnit->bufferList;
|
PLENTRY entry = decodeUnit->bufferList;
|
||||||
while (entry != NULL) {
|
while (entry != NULL) {
|
||||||
memcpy(&data[offset], entry->data, entry->length);
|
// Submit parameter set NALUs directly since no copy is required by the decoder
|
||||||
offset += entry->length;
|
if (entry->bufferType != BUFFER_TYPE_PICDATA) {
|
||||||
|
ret = [renderer submitDecodeBuffer:(unsigned char*)entry->data length:entry->length bufferType:entry->bufferType];
|
||||||
|
if (ret != DR_OK) {
|
||||||
|
free(data);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
memcpy(&data[offset], entry->data, entry->length);
|
||||||
|
offset += entry->length;
|
||||||
|
}
|
||||||
|
|
||||||
entry = entry->next;
|
entry = entry->next;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function will take our buffer
|
// This function will take our picture data buffer
|
||||||
return [renderer submitDecodeBuffer:data length:decodeUnit->fullLength];
|
return [renderer submitDecodeBuffer:data length:offset bufferType:BUFFER_TYPE_PICDATA];
|
||||||
}
|
}
|
||||||
|
|
||||||
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, void* context, int flags)
|
||||||
{
|
{
|
||||||
int err;
|
int err;
|
||||||
|
|
||||||
// We only support stereo for now
|
// Clear the circular buffer
|
||||||
|
audioBufferWriteIndex = audioBufferReadIndex = 0;
|
||||||
|
|
||||||
|
// We only support stereo for now.
|
||||||
|
// TODO: Ensure AudioToolbox's channel mapping matches Opus's
|
||||||
|
// and correct if neccessary.
|
||||||
assert(audioConfiguration == AUDIO_CONFIGURATION_STEREO);
|
assert(audioConfiguration == AUDIO_CONFIGURATION_STEREO);
|
||||||
|
|
||||||
opusDecoder = opus_decoder_create(opusConfig->sampleRate,
|
activeChannelCount = opusConfig->channelCount;
|
||||||
opusConfig->channelCount,
|
opusDecoder = opus_multistream_decoder_create(opusConfig->sampleRate,
|
||||||
&err);
|
opusConfig->channelCount,
|
||||||
|
opusConfig->streams,
|
||||||
audioLock = [[NSLock alloc] init];
|
opusConfig->coupledStreams,
|
||||||
|
opusConfig->mapping,
|
||||||
|
&err);
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
// Configure the audio session for our app
|
// Configure the audio session for our app
|
||||||
NSError *audioSessionError = nil;
|
NSError *audioSessionError = nil;
|
||||||
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
||||||
@@ -94,23 +116,10 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
|
|||||||
[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
|
[audioSession setPreferredOutputNumberOfChannels:opusConfig->channelCount error:&audioSessionError];
|
||||||
[audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
|
[audioSession setPreferredIOBufferDuration:0.005 error:&audioSessionError];
|
||||||
[audioSession setActive: YES error: &audioSessionError];
|
[audioSession setActive: YES error: &audioSessionError];
|
||||||
|
#endif
|
||||||
|
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
|
|
||||||
AudioComponentDescription audioDesc;
|
|
||||||
audioDesc.componentType = kAudioUnitType_Output;
|
|
||||||
audioDesc.componentSubType = kAudioUnitSubType_RemoteIO;
|
|
||||||
audioDesc.componentFlags = 0;
|
|
||||||
audioDesc.componentFlagsMask = 0;
|
|
||||||
audioDesc.componentManufacturer = kAudioUnitManufacturer_Apple;
|
|
||||||
|
|
||||||
status = AudioComponentInstanceNew(AudioComponentFindNext(NULL, &audioDesc), &audioUnit);
|
|
||||||
|
|
||||||
if (status) {
|
|
||||||
Log(LOG_E, @"Unable to instantiate new AudioComponent: %d", (int32_t)status);
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
AudioStreamBasicDescription audioFormat = {0};
|
AudioStreamBasicDescription audioFormat = {0};
|
||||||
audioFormat.mSampleRate = opusConfig->sampleRate;
|
audioFormat.mSampleRate = opusConfig->sampleRate;
|
||||||
audioFormat.mBitsPerChannel = 16;
|
audioFormat.mBitsPerChannel = 16;
|
||||||
@@ -122,41 +131,25 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
|
|||||||
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
|
audioFormat.mFramesPerPacket = audioFormat.mBytesPerPacket / audioFormat.mBytesPerFrame;
|
||||||
audioFormat.mReserved = 0;
|
audioFormat.mReserved = 0;
|
||||||
|
|
||||||
status = AudioUnitSetProperty(audioUnit,
|
status = AudioQueueNewOutput(&audioFormat, FillOutputBuffer, nil, nil, nil, 0, &audioQueue);
|
||||||
kAudioUnitProperty_StreamFormat,
|
if (status != noErr) {
|
||||||
kAudioUnitScope_Input,
|
Log(LOG_E, @"Error allocating output queue: %d\n", status);
|
||||||
OUTPUT_BUS,
|
|
||||||
&audioFormat,
|
|
||||||
sizeof(audioFormat));
|
|
||||||
if (status) {
|
|
||||||
Log(LOG_E, @"Unable to set audio unit to input: %d", (int32_t)status);
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
AURenderCallbackStruct callbackStruct = {0};
|
for (int i = 0; i < AUDIO_QUEUE_BUFFERS; i++) {
|
||||||
callbackStruct.inputProc = playbackCallback;
|
status = AudioQueueAllocateBuffer(audioQueue, audioFormat.mBytesPerFrame * FRAME_SIZE, &audioBuffers[i]);
|
||||||
callbackStruct.inputProcRefCon = NULL;
|
if (status != noErr) {
|
||||||
|
Log(LOG_E, @"Error allocating output buffer: %d\n", status);
|
||||||
|
return status;
|
||||||
|
}
|
||||||
|
|
||||||
status = AudioUnitSetProperty(audioUnit,
|
FillOutputBuffer(nil, audioQueue, audioBuffers[i]);
|
||||||
kAudioUnitProperty_SetRenderCallback,
|
|
||||||
kAudioUnitScope_Input,
|
|
||||||
OUTPUT_BUS,
|
|
||||||
&callbackStruct,
|
|
||||||
sizeof(callbackStruct));
|
|
||||||
if (status) {
|
|
||||||
Log(LOG_E, @"Unable to set audio unit callback: %d", (int32_t)status);
|
|
||||||
return status;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
status = AudioUnitInitialize(audioUnit);
|
status = AudioQueueStart(audioQueue, nil);
|
||||||
if (status) {
|
if (status != noErr) {
|
||||||
Log(LOG_E, @"Unable to initialize audioUnit: %d", (int32_t)status);
|
Log(LOG_E, @"Error starting queue: %d\n", status);
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
status = AudioOutputUnitStart(audioUnit);
|
|
||||||
if (status) {
|
|
||||||
Log(LOG_E, @"Unable to start audioUnit: %d", (int32_t)status);
|
|
||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,77 +159,44 @@ int ArInit(int audioConfiguration, POPUS_MULTISTREAM_CONFIGURATION opusConfig, v
|
|||||||
void ArCleanup(void)
|
void ArCleanup(void)
|
||||||
{
|
{
|
||||||
if (opusDecoder != NULL) {
|
if (opusDecoder != NULL) {
|
||||||
opus_decoder_destroy(opusDecoder);
|
opus_multistream_decoder_destroy(opusDecoder);
|
||||||
opusDecoder = NULL;
|
opusDecoder = NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
OSStatus status = AudioOutputUnitStop(audioUnit);
|
// Stop before disposing to avoid massive delay inside
|
||||||
if (status) {
|
// AudioQueueDispose() (iOS bug?)
|
||||||
Log(LOG_E, @"Unable to stop audioUnit: %d", (int32_t)status);
|
AudioQueueStop(audioQueue, true);
|
||||||
}
|
|
||||||
|
|
||||||
status = AudioUnitUninitialize(audioUnit);
|
// Also frees buffers
|
||||||
if (status) {
|
AudioQueueDispose(audioQueue, true);
|
||||||
Log(LOG_E, @"Unable to uninitialize audioUnit: %d", (int32_t)status);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
// Audio session is now inactive
|
// Audio session is now inactive
|
||||||
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
AVAudioSession* audioSession = [AVAudioSession sharedInstance];
|
||||||
[audioSession setActive: YES error: nil];
|
[audioSession setActive: YES error: nil];
|
||||||
|
#endif
|
||||||
// This is safe because we're guaranteed that nobody
|
|
||||||
// is touching this list now
|
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
|
|
||||||
while (audioBufferQueue != NULL) {
|
|
||||||
entry = audioBufferQueue;
|
|
||||||
audioBufferQueue = entry->next;
|
|
||||||
audioBufferQueueLength--;
|
|
||||||
free(entry);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
void ArDecodeAndPlaySample(char* sampleData, int sampleLength)
|
||||||
{
|
{
|
||||||
int decodedLength = opus_decode(opusDecoder, (unsigned char*)sampleData, sampleLength, decodedPcmBuffer, PCM_BUFFER_SIZE / 2, 0);
|
int decodeLen;
|
||||||
if (decodedLength > 0) {
|
|
||||||
// Return of opus_decode is samples per channel
|
|
||||||
decodedLength *= 4;
|
|
||||||
|
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *newEntry = malloc(sizeof(*newEntry) + decodedLength);
|
// Check if there is space for this sample in the buffer. Again, this can race
|
||||||
if (newEntry != NULL) {
|
// but in the worst case, we'll not see the sample callback having consumed a sample.
|
||||||
newEntry->next = NULL;
|
if (((audioBufferWriteIndex + 1) % CIRCULAR_BUFFER_SIZE) == audioBufferReadIndex) {
|
||||||
newEntry->length = decodedLength;
|
return;
|
||||||
newEntry->offset = 0;
|
}
|
||||||
memcpy(newEntry->data, decodedPcmBuffer, decodedLength);
|
|
||||||
|
|
||||||
[audioLock lock];
|
decodeLen = opus_multistream_decode(opusDecoder, (unsigned char *)sampleData, sampleLength,
|
||||||
if (audioBufferQueueLength > MAX_QUEUE_ENTRIES) {
|
audioCircularBuffer[audioBufferWriteIndex], FRAME_SIZE, 0);
|
||||||
Log(LOG_W, @"Audio player too slow. Dropping all decoded samples!");
|
if (decodeLen > 0) {
|
||||||
|
// Use a full memory barrier to ensure the circular buffer is written before incrementing the index
|
||||||
|
__sync_synchronize();
|
||||||
|
|
||||||
// Clear all values from the buffer queue
|
// This can race with the reader in the sample callback, however this is a benign
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *entry;
|
// race since we'll either read the original value of s_WriteIndex (which is safe,
|
||||||
while (audioBufferQueue != NULL) {
|
// we just won't consider this sample) or the new value of s_WriteIndex
|
||||||
entry = audioBufferQueue;
|
audioBufferWriteIndex = (audioBufferWriteIndex + 1) % CIRCULAR_BUFFER_SIZE;
|
||||||
audioBufferQueue = entry->next;
|
|
||||||
audioBufferQueueLength--;
|
|
||||||
free(entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (audioBufferQueue == NULL) {
|
|
||||||
audioBufferQueue = newEntry;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *lastEntry = audioBufferQueue;
|
|
||||||
while (lastEntry->next != NULL) {
|
|
||||||
lastEntry = lastEntry->next;
|
|
||||||
}
|
|
||||||
lastEntry->next = newEntry;
|
|
||||||
}
|
|
||||||
audioBufferQueueLength++;
|
|
||||||
|
|
||||||
[audioLock unlock];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -265,16 +225,6 @@ void ClConnectionTerminated(long errorCode)
|
|||||||
[_callbacks connectionTerminated: errorCode];
|
[_callbacks connectionTerminated: errorCode];
|
||||||
}
|
}
|
||||||
|
|
||||||
void ClDisplayMessage(const char* message)
|
|
||||||
{
|
|
||||||
[_callbacks displayMessage: message];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClDisplayTransientMessage(const char* message)
|
|
||||||
{
|
|
||||||
[_callbacks displayTransientMessage: message];
|
|
||||||
}
|
|
||||||
|
|
||||||
void ClLogMessage(const char* format, ...)
|
void ClLogMessage(const char* format, ...)
|
||||||
{
|
{
|
||||||
va_list va;
|
va_list va;
|
||||||
@@ -283,8 +233,19 @@ void ClLogMessage(const char* format, ...)
|
|||||||
va_end(va);
|
va_end(va);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ClRumble(unsigned short controllerNumber, unsigned short lowFreqMotor, unsigned short highFreqMotor)
|
||||||
|
{
|
||||||
|
[_callbacks rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
|
||||||
|
}
|
||||||
|
|
||||||
-(void) terminate
|
-(void) terminate
|
||||||
{
|
{
|
||||||
|
// Interrupt any action blocking LiStartConnection(). This is
|
||||||
|
// thread-safe and done outside initLock on purpose, since we
|
||||||
|
// won't be able to acquire it if LiStartConnection is in
|
||||||
|
// progress.
|
||||||
|
LiInterruptConnection();
|
||||||
|
|
||||||
// We dispatch this async to get out because this can be invoked
|
// We dispatch this async to get out because this can be invoked
|
||||||
// on a thread inside common and we don't want to deadlock. It also avoids
|
// on a thread inside common and we don't want to deadlock. It also avoids
|
||||||
// blocking on the caller's thread waiting to acquire initLock.
|
// blocking on the caller's thread waiting to acquire initLock.
|
||||||
@@ -305,11 +266,23 @@ void ClLogMessage(const char* format, ...)
|
|||||||
initLock = [[NSLock alloc] init];
|
initLock = [[NSLock alloc] init];
|
||||||
}
|
}
|
||||||
|
|
||||||
LiInitializeServerInformation(&_serverInfo);
|
strncpy(_hostString,
|
||||||
_serverInfo.address = [config.host cStringUsingEncoding:NSUTF8StringEncoding];
|
[config.host cStringUsingEncoding:NSUTF8StringEncoding],
|
||||||
_serverInfo.serverInfoAppVersion = [config.appVersion cStringUsingEncoding:NSUTF8StringEncoding];
|
sizeof(_hostString));
|
||||||
|
strncpy(_appVersionString,
|
||||||
|
[config.appVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
sizeof(_appVersionString));
|
||||||
if (config.gfeVersion != nil) {
|
if (config.gfeVersion != nil) {
|
||||||
_serverInfo.serverInfoGfeVersion = [config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding];
|
strncpy(_gfeVersionString,
|
||||||
|
[config.gfeVersion cStringUsingEncoding:NSUTF8StringEncoding],
|
||||||
|
sizeof(_gfeVersionString));
|
||||||
|
}
|
||||||
|
|
||||||
|
LiInitializeServerInformation(&_serverInfo);
|
||||||
|
_serverInfo.address = _hostString;
|
||||||
|
_serverInfo.serverInfoAppVersion = _appVersionString;
|
||||||
|
if (config.gfeVersion != nil) {
|
||||||
|
_serverInfo.serverInfoGfeVersion = _gfeVersionString;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderer = myRenderer;
|
renderer = myRenderer;
|
||||||
@@ -320,15 +293,53 @@ void ClLogMessage(const char* format, ...)
|
|||||||
_streamConfig.height = config.height;
|
_streamConfig.height = config.height;
|
||||||
_streamConfig.fps = config.frameRate;
|
_streamConfig.fps = config.frameRate;
|
||||||
_streamConfig.bitrate = config.bitRate;
|
_streamConfig.bitrate = config.bitRate;
|
||||||
|
_streamConfig.enableHdr = config.enableHdr;
|
||||||
|
|
||||||
// On iOS 11, we can use HEVC if the server supports encoding it
|
// Use some of the HEVC encoding efficiency improvements to
|
||||||
// and this device has hardware decode for it (A9 and later)
|
// reduce bandwidth usage while still gaining some image
|
||||||
if (@available(iOS 11.0, *)) {
|
// quality improvement.
|
||||||
_streamConfig.supportsHevc = VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
|
_streamConfig.hevcBitratePercentageMultiplier = 75;
|
||||||
|
|
||||||
|
// Detect remote streaming automatically based on the IP address of the target
|
||||||
|
_streamConfig.streamingRemotely = STREAM_CFG_AUTO;
|
||||||
|
_streamConfig.packetSize = 1392;
|
||||||
|
|
||||||
|
switch (config.audioChannelCount) {
|
||||||
|
case 2:
|
||||||
|
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_STEREO;
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
_streamConfig.audioConfiguration = AUDIO_CONFIGURATION_51_SURROUND;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log(LOG_E, @"Unknown audio channel count: %d", config.audioChannelCount);
|
||||||
|
abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: We should use 1024 when streaming remotely
|
// HDR implies HEVC allowed
|
||||||
_streamConfig.packetSize = 1292;
|
if (config.enableHdr) {
|
||||||
|
config.allowHevc = YES;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
|
// On iOS 11, we can use HEVC if the server supports encoding it
|
||||||
|
// and this device has hardware decode for it (A9 and later).
|
||||||
|
// Additionally, iPhone X had a bug which would cause video
|
||||||
|
// to freeze after a few minutes with HEVC prior to iOS 11.3.
|
||||||
|
// As a result, we will only use HEVC on iOS 11.3 or later.
|
||||||
|
if (@available(iOS 11.3, *)) {
|
||||||
|
_streamConfig.supportsHevc = config.allowHevc && VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC);
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if (@available(macOS 10.13, *)) {
|
||||||
|
// Streaming with limited bandwidth will result in better quality with HEVC
|
||||||
|
if (VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) || _streamConfig.streamingRemotely != 0)
|
||||||
|
_streamConfig.supportsHevc = config.allowHevc;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// HEVC must be supported when HDR is enabled
|
||||||
|
assert(!_streamConfig.enableHdr || _streamConfig.supportsHevc);
|
||||||
|
|
||||||
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
|
memcpy(_streamConfig.remoteInputAesKey, [config.riKey bytes], [config.riKey length]);
|
||||||
memset(_streamConfig.remoteInputAesIv, 0, 16);
|
memset(_streamConfig.remoteInputAesIv, 0, 16);
|
||||||
@@ -340,12 +351,15 @@ void ClLogMessage(const char* format, ...)
|
|||||||
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
|
_drCallbacks.submitDecodeUnit = DrSubmitDecodeUnit;
|
||||||
|
|
||||||
// RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
|
// RFI doesn't work properly with HEVC on iOS 11 with an iPhone SE (at least)
|
||||||
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC;
|
// It doesnt work on macOS either, tested with Network Link Conditioner.
|
||||||
|
_drCallbacks.capabilities = CAPABILITY_REFERENCE_FRAME_INVALIDATION_AVC |
|
||||||
|
CAPABILITY_DIRECT_SUBMIT;
|
||||||
|
|
||||||
LiInitializeAudioCallbacks(&_arCallbacks);
|
LiInitializeAudioCallbacks(&_arCallbacks);
|
||||||
_arCallbacks.init = ArInit;
|
_arCallbacks.init = ArInit;
|
||||||
_arCallbacks.cleanup = ArCleanup;
|
_arCallbacks.cleanup = ArCleanup;
|
||||||
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
_arCallbacks.decodeAndPlaySample = ArDecodeAndPlaySample;
|
||||||
|
_arCallbacks.capabilities = CAPABILITY_DIRECT_SUBMIT;
|
||||||
|
|
||||||
LiInitializeConnectionCallbacks(&_clCallbacks);
|
LiInitializeConnectionCallbacks(&_clCallbacks);
|
||||||
_clCallbacks.stageStarting = ClStageStarting;
|
_clCallbacks.stageStarting = ClStageStarting;
|
||||||
@@ -353,94 +367,41 @@ void ClLogMessage(const char* format, ...)
|
|||||||
_clCallbacks.stageFailed = ClStageFailed;
|
_clCallbacks.stageFailed = ClStageFailed;
|
||||||
_clCallbacks.connectionStarted = ClConnectionStarted;
|
_clCallbacks.connectionStarted = ClConnectionStarted;
|
||||||
_clCallbacks.connectionTerminated = ClConnectionTerminated;
|
_clCallbacks.connectionTerminated = ClConnectionTerminated;
|
||||||
_clCallbacks.displayMessage = ClDisplayMessage;
|
|
||||||
_clCallbacks.displayTransientMessage = ClDisplayTransientMessage;
|
|
||||||
_clCallbacks.logMessage = ClLogMessage;
|
_clCallbacks.logMessage = ClLogMessage;
|
||||||
|
_clCallbacks.rumble = ClRumble;
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
static OSStatus playbackCallback(void *inRefCon,
|
static void FillOutputBuffer(void *aqData,
|
||||||
AudioUnitRenderActionFlags *ioActionFlags,
|
AudioQueueRef inAQ,
|
||||||
const AudioTimeStamp *inTimeStamp,
|
AudioQueueBufferRef inBuffer) {
|
||||||
UInt32 inBusNumber,
|
inBuffer->mAudioDataByteSize = activeChannelCount * FRAME_SIZE * sizeof(short);
|
||||||
UInt32 inNumberFrames,
|
|
||||||
AudioBufferList *ioData) {
|
|
||||||
// Notes: ioData contains buffers (may be more than one!)
|
|
||||||
// Fill them up as much as you can. Remember to set the size value in each buffer to match how
|
|
||||||
// much data is in the buffer.
|
|
||||||
|
|
||||||
bool ranOutOfData = false;
|
assert(inBuffer->mAudioDataByteSize == inBuffer->mAudioDataBytesCapacity);
|
||||||
for (int i = 0; i < ioData->mNumberBuffers; i++) {
|
|
||||||
ioData->mBuffers[i].mNumberChannels = 2;
|
|
||||||
|
|
||||||
if (ranOutOfData) {
|
// If the indexes aren't equal, we have a sample
|
||||||
ioData->mBuffers[i].mDataByteSize = 0;
|
if (audioBufferWriteIndex != audioBufferReadIndex) {
|
||||||
continue;
|
// Copy data to the audio buffer
|
||||||
}
|
memcpy(inBuffer->mAudioData,
|
||||||
|
audioCircularBuffer[audioBufferReadIndex],
|
||||||
|
inBuffer->mAudioDataByteSize);
|
||||||
|
|
||||||
if (ioData->mBuffers[i].mDataByteSize != 0) {
|
// Use a full memory barrier to ensure the circular buffer is read before incrementing the index
|
||||||
int thisBufferOffset = 0;
|
__sync_synchronize();
|
||||||
|
|
||||||
FillBufferAgain:
|
// This can race with the reader in the AudDecDecodeAndPlaySample function. This is
|
||||||
// Make sure there's data to write
|
// not a problem because at worst, it just won't see that we've consumed this sample yet.
|
||||||
if (ioData->mBuffers[i].mDataByteSize - thisBufferOffset == 0) {
|
audioBufferReadIndex = (audioBufferReadIndex + 1) % CIRCULAR_BUFFER_SIZE;
|
||||||
continue;
|
}
|
||||||
}
|
else {
|
||||||
|
// No data, so play silence
|
||||||
struct AUDIO_BUFFER_QUEUE_ENTRY *audioEntry = NULL;
|
memset(inBuffer->mAudioData, 0, inBuffer->mAudioDataByteSize);
|
||||||
|
|
||||||
[audioLock lock];
|
|
||||||
if (audioBufferQueue != NULL) {
|
|
||||||
// Dequeue this entry temporarily
|
|
||||||
audioEntry = audioBufferQueue;
|
|
||||||
audioBufferQueue = audioBufferQueue->next;
|
|
||||||
audioBufferQueueLength--;
|
|
||||||
}
|
|
||||||
[audioLock unlock];
|
|
||||||
|
|
||||||
if (audioEntry == NULL) {
|
|
||||||
// No data left
|
|
||||||
ranOutOfData = true;
|
|
||||||
ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Figure out how much data we can write
|
|
||||||
int min = MIN(ioData->mBuffers[i].mDataByteSize - thisBufferOffset, audioEntry->length);
|
|
||||||
|
|
||||||
// Copy data to the audio buffer
|
|
||||||
memcpy(&ioData->mBuffers[i].mData[thisBufferOffset], &audioEntry->data[audioEntry->offset], min);
|
|
||||||
thisBufferOffset += min;
|
|
||||||
|
|
||||||
if (min < audioEntry->length) {
|
|
||||||
// This entry still has unused data
|
|
||||||
audioEntry->length -= min;
|
|
||||||
audioEntry->offset += min;
|
|
||||||
|
|
||||||
// Requeue the entry
|
|
||||||
[audioLock lock];
|
|
||||||
audioEntry->next = audioBufferQueue;
|
|
||||||
audioBufferQueue = audioEntry;
|
|
||||||
audioBufferQueueLength++;
|
|
||||||
[audioLock unlock];
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// This entry is fully depleted so free it
|
|
||||||
free(audioEntry);
|
|
||||||
|
|
||||||
// Try to grab another sample to fill this buffer with
|
|
||||||
goto FillBufferAgain;
|
|
||||||
}
|
|
||||||
|
|
||||||
ioData->mBuffers[i].mDataByteSize = thisBufferOffset;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return noErr;
|
AudioQueueEnqueueBuffer(inAQ, inBuffer, 0, NULL);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
-(void) main
|
-(void) main
|
||||||
{
|
{
|
||||||
[initLock lock];
|
[initLock lock];
|
||||||
|
|||||||
@@ -6,19 +6,28 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@interface StreamConfiguration : NSObject
|
@interface StreamConfiguration : NSObject
|
||||||
|
|
||||||
@property NSString* host;
|
@property NSString* host;
|
||||||
@property NSString* appVersion;
|
@property NSString* appVersion;
|
||||||
@property NSString* gfeVersion;
|
@property NSString* gfeVersion;
|
||||||
@property NSString* appID;
|
@property NSString* appID;
|
||||||
|
@property NSString* appName;
|
||||||
@property int width;
|
@property int width;
|
||||||
@property int height;
|
@property int height;
|
||||||
@property int frameRate;
|
@property int frameRate;
|
||||||
@property int bitRate;
|
@property int bitRate;
|
||||||
@property int riKeyId;
|
@property int riKeyId;
|
||||||
|
@property BOOL streamingRemotely;
|
||||||
@property NSData* riKey;
|
@property NSData* riKey;
|
||||||
|
@property int gamepadMask;
|
||||||
|
@property BOOL optimizeGameSettings;
|
||||||
|
@property BOOL playAudioOnPC;
|
||||||
|
@property int audioChannelCount;
|
||||||
|
@property int audioChannelMask;
|
||||||
|
@property BOOL enableHdr;
|
||||||
|
@property BOOL multiController;
|
||||||
|
@property BOOL allowHevc;
|
||||||
|
@property NSData* serverCert;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -9,5 +9,5 @@
|
|||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
|
|
||||||
@implementation StreamConfiguration
|
@implementation StreamConfiguration
|
||||||
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey;
|
@synthesize host, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, streamingRemotely, appName, optimizeGameSettings, playAudioOnPC, audioChannelMask, audioChannelCount, enableHdr, multiController, allowHevc, serverCert;
|
||||||
@end
|
@end
|
||||||
@@ -6,13 +6,13 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
#import "StreamConfiguration.h"
|
#import "StreamConfiguration.h"
|
||||||
#import "Connection.h"
|
#import "Connection.h"
|
||||||
|
|
||||||
@interface StreamManager : NSOperation
|
@interface StreamManager : NSOperation
|
||||||
|
|
||||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
|
||||||
|
|
||||||
- (void) stopStream;
|
- (void) stopStream;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
#import "CryptoManager.h"
|
#import "CryptoManager.h"
|
||||||
#import "HttpManager.h"
|
#import "HttpManager.h"
|
||||||
#import "Utils.h"
|
#import "Utils.h"
|
||||||
#import "OnScreenControls.h"
|
|
||||||
#import "StreamView.h"
|
#import "StreamView.h"
|
||||||
#import "ServerInfoResponse.h"
|
#import "ServerInfoResponse.h"
|
||||||
#import "HttpResponse.h"
|
#import "HttpResponse.h"
|
||||||
@@ -19,12 +19,13 @@
|
|||||||
|
|
||||||
@implementation StreamManager {
|
@implementation StreamManager {
|
||||||
StreamConfiguration* _config;
|
StreamConfiguration* _config;
|
||||||
UIView* _renderView;
|
|
||||||
|
OSView* _renderView;
|
||||||
id<ConnectionCallbacks> _callbacks;
|
id<ConnectionCallbacks> _callbacks;
|
||||||
Connection* _connection;
|
Connection* _connection;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
|
- (id) initWithConfig:(StreamConfiguration*)config renderView:(OSView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
|
||||||
self = [super init];
|
self = [super init];
|
||||||
_config = config;
|
_config = config;
|
||||||
_renderView = view;
|
_renderView = view;
|
||||||
@@ -34,19 +35,16 @@
|
|||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
- (void)main {
|
- (void)main {
|
||||||
[CryptoManager generateKeyPairUsingSSl];
|
[CryptoManager generateKeyPairUsingSSL];
|
||||||
NSString* uniqueId = [IdManager getUniqueId];
|
NSString* uniqueId = [IdManager getUniqueId];
|
||||||
NSData* cert = [CryptoManager readCertFromFile];
|
|
||||||
|
|
||||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
|
HttpManager* hMan = [[HttpManager alloc] initWithHost:_config.host
|
||||||
uniqueId:uniqueId
|
uniqueId:uniqueId
|
||||||
deviceName:@"roth"
|
serverCert:_config.serverCert];
|
||||||
cert:cert];
|
|
||||||
|
|
||||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest]
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
|
||||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||||
NSString* pairStatus = [serverInfoResp getStringTag:@"PairStatus"];
|
NSString* pairStatus = [serverInfoResp getStringTag:@"PairStatus"];
|
||||||
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
NSString* appversion = [serverInfoResp getStringTag:@"appversion"];
|
||||||
@@ -76,21 +74,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
// Set mouse delta factors from the screen resolution and stream size
|
// Set mouse delta factors from the screen resolution and stream size
|
||||||
CGFloat screenScale = [[UIScreen mainScreen] scale];
|
CGFloat screenScale = [[UIScreen mainScreen] scale];
|
||||||
CGRect screenBounds = [[UIScreen mainScreen] bounds];
|
CGRect screenBounds = [[UIScreen mainScreen] bounds];
|
||||||
CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);
|
CGSize screenSize = CGSizeMake(screenBounds.size.width * screenScale, screenBounds.size.height * screenScale);
|
||||||
[((StreamView*)_renderView) setMouseDeltaFactors:_config.width / screenSize.width
|
[((StreamView*)_renderView) setMouseDeltaFactors:_config.width / screenSize.width
|
||||||
y:_config.height / screenSize.height];
|
y:_config.height / screenSize.height];
|
||||||
|
#endif
|
||||||
|
|
||||||
// Populate the config's version fields from serverinfo
|
// Populate the config's version fields from serverinfo
|
||||||
_config.appVersion = appversion;
|
_config.appVersion = appversion;
|
||||||
_config.gfeVersion = gfeVersion;
|
_config.gfeVersion = gfeVersion;
|
||||||
|
|
||||||
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc]initWithView:_renderView];
|
// Initializing the renderer must be done on the main thread
|
||||||
_connection = [[Connection alloc] initWithConfig:_config renderer:renderer connectionCallbacks:_callbacks];
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView];
|
||||||
[opQueue addOperation:_connection];
|
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks];
|
||||||
|
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
|
||||||
|
[opQueue addOperation:self->_connection];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) stopStream
|
- (void) stopStream
|
||||||
@@ -100,21 +103,15 @@
|
|||||||
|
|
||||||
- (BOOL) launchApp:(HttpManager*)hMan {
|
- (BOOL) launchApp:(HttpManager*)hMan {
|
||||||
HttpResponse* launchResp = [[HttpResponse alloc] init];
|
HttpResponse* launchResp = [[HttpResponse alloc] init];
|
||||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:launchResp withUrlRequest:[hMan newLaunchRequest:_config]]];
|
||||||
[hMan newLaunchRequest:_config.appID
|
|
||||||
width:_config.width
|
|
||||||
height:_config.height
|
|
||||||
refreshRate:_config.frameRate
|
|
||||||
rikey:[Utils bytesToHex:_config.riKey]
|
|
||||||
rikeyid:_config.riKeyId]]];
|
|
||||||
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
|
NSString *gameSession = [launchResp getStringTag:@"gamesession"];
|
||||||
if (launchResp == NULL || ![launchResp isStatusOk]) {
|
if (![launchResp isStatusOk]) {
|
||||||
[_callbacks launchFailed:@"Failed to launch app"];
|
[_callbacks launchFailed:launchResp.statusMessage];
|
||||||
Log(LOG_E, @"Failed Launch Response: %@", launchResp.statusMessage);
|
Log(LOG_E, @"Failed Launch Response: %@", launchResp.statusMessage);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
|
} else if (gameSession == NULL || [gameSession isEqualToString:@"0"]) {
|
||||||
[_callbacks launchFailed:launchResp.statusMessage];
|
[_callbacks launchFailed:@"Failed to launch app"];
|
||||||
Log(LOG_E, @"Failed to parse game session. Code: %ld Response: %@", (long)launchResp.statusCode, launchResp.statusMessage);
|
Log(LOG_E, @"Failed to parse game session");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,17 +120,15 @@
|
|||||||
|
|
||||||
- (BOOL) resumeApp:(HttpManager*)hMan {
|
- (BOOL) resumeApp:(HttpManager*)hMan {
|
||||||
HttpResponse* resumeResp = [[HttpResponse alloc] init];
|
HttpResponse* resumeResp = [[HttpResponse alloc] init];
|
||||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:resumeResp withUrlRequest:[hMan newResumeRequest:_config]]];
|
||||||
[hMan newResumeRequestWithRiKey:[Utils bytesToHex:_config.riKey]
|
|
||||||
riKeyId:_config.riKeyId]]];
|
|
||||||
NSString* resume = [resumeResp getStringTag:@"resume"];
|
NSString* resume = [resumeResp getStringTag:@"resume"];
|
||||||
if (resumeResp == NULL || ![resumeResp isStatusOk]) {
|
if (![resumeResp isStatusOk]) {
|
||||||
[_callbacks launchFailed:@"Failed to resume app"];
|
[_callbacks launchFailed:resumeResp.statusMessage];
|
||||||
Log(LOG_E, @"Failed Resume Response: %@", resumeResp.statusMessage);
|
Log(LOG_E, @"Failed Resume Response: %@", resumeResp.statusMessage);
|
||||||
return FALSE;
|
return FALSE;
|
||||||
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
|
} else if (resume == NULL || [resume isEqualToString:@"0"]) {
|
||||||
[_callbacks launchFailed:resumeResp.statusMessage];
|
[_callbacks launchFailed:@"Failed to resume app"];
|
||||||
Log(LOG_E, @"Failed to parse resume response. Code: %ld Response: %@", (long)resumeResp.statusCode, resumeResp.statusMessage);
|
Log(LOG_E, @"Failed to parse resume response");
|
||||||
return FALSE;
|
return FALSE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,16 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@import AVFoundation;
|
@import AVFoundation;
|
||||||
|
|
||||||
@interface VideoDecoderRenderer : NSObject
|
@interface VideoDecoderRenderer : NSObject
|
||||||
|
|
||||||
- (id)initWithView:(UIView*)view;
|
- (id)initWithView:(OSView*)view;
|
||||||
|
|
||||||
- (void)setupWithVideoFormat:(int)videoFormat;
|
- (void)setupWithVideoFormat:(int)videoFormat;
|
||||||
|
|
||||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
|
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength;
|
||||||
|
|
||||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length;
|
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -7,11 +7,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "VideoDecoderRenderer.h"
|
#import "VideoDecoderRenderer.h"
|
||||||
|
#import "StreamView.h"
|
||||||
|
|
||||||
#include "Limelight.h"
|
#include "Limelight.h"
|
||||||
|
|
||||||
@implementation VideoDecoderRenderer {
|
@implementation VideoDecoderRenderer {
|
||||||
UIView *_view;
|
StreamView* _view;
|
||||||
|
|
||||||
AVSampleBufferDisplayLayer* displayLayer;
|
AVSampleBufferDisplayLayer* displayLayer;
|
||||||
Boolean waitingForSps, waitingForPps, waitingForVps;
|
Boolean waitingForSps, waitingForPps, waitingForVps;
|
||||||
@@ -27,10 +28,15 @@
|
|||||||
|
|
||||||
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
|
||||||
displayLayer.bounds = _view.bounds;
|
displayLayer.bounds = _view.bounds;
|
||||||
displayLayer.backgroundColor = [UIColor blackColor].CGColor;
|
displayLayer.backgroundColor = [OSColor blackColor].CGColor;
|
||||||
|
|
||||||
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
|
||||||
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
displayLayer.videoGravity = AVLayerVideoGravityResizeAspect;
|
||||||
|
|
||||||
|
// Hide the layer until we get an IDR frame. This ensures we
|
||||||
|
// can see the loading progress label as the stream is starting.
|
||||||
|
displayLayer.hidden = YES;
|
||||||
|
|
||||||
if (oldLayer != nil) {
|
if (oldLayer != nil) {
|
||||||
// Switch out the old display layer with the new one
|
// Switch out the old display layer with the new one
|
||||||
[_view.layer replaceSublayer:oldLayer with:displayLayer];
|
[_view.layer replaceSublayer:oldLayer with:displayLayer];
|
||||||
@@ -53,7 +59,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (id)initWithView:(UIView*)view
|
- (id)initWithView:(StreamView*)view
|
||||||
{
|
{
|
||||||
self = [super init];
|
self = [super init];
|
||||||
|
|
||||||
@@ -67,24 +73,18 @@
|
|||||||
- (void)setupWithVideoFormat:(int)videoFormat
|
- (void)setupWithVideoFormat:(int)videoFormat
|
||||||
{
|
{
|
||||||
self->videoFormat = videoFormat;
|
self->videoFormat = videoFormat;
|
||||||
|
#if !TARGET_OS_IPHONE
|
||||||
|
_view.codec = videoFormat;
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
#define FRAME_START_PREFIX_SIZE 4
|
#define FRAME_START_PREFIX_SIZE 4
|
||||||
#define NALU_START_PREFIX_SIZE 3
|
#define NALU_START_PREFIX_SIZE 3
|
||||||
#define NAL_LENGTH_PREFIX_SIZE 4
|
#define NAL_LENGTH_PREFIX_SIZE 4
|
||||||
#define AVC_NAL_TYPE_SPS 0x67
|
|
||||||
#define AVC_NAL_TYPE_PPS 0x68
|
|
||||||
#define HEVC_NAL_TYPE_VPS 0x40
|
|
||||||
#define HEVC_NAL_TYPE_SPS 0x42
|
|
||||||
#define HEVC_NAL_TYPE_PPS 0x44
|
|
||||||
|
|
||||||
#define IS_NAL_SPS(x) ((videoFormat == VIDEO_FORMAT_H264) ? ((x) == AVC_NAL_TYPE_SPS) : ((x) == HEVC_NAL_TYPE_SPS))
|
|
||||||
#define IS_NAL_PPS(x) ((videoFormat == VIDEO_FORMAT_H264) ? ((x) == AVC_NAL_TYPE_PPS) : ((x) == HEVC_NAL_TYPE_PPS))
|
|
||||||
#define IS_NAL_VPS(x) ((videoFormat == VIDEO_FORMAT_H265) && ((x) == HEVC_NAL_TYPE_VPS))
|
|
||||||
|
|
||||||
- (Boolean)readyForPictureData
|
- (Boolean)readyForPictureData
|
||||||
{
|
{
|
||||||
if (videoFormat == VIDEO_FORMAT_H264) {
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
return !waitingForSps && !waitingForPps;
|
return !waitingForSps && !waitingForPps;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -95,7 +95,7 @@
|
|||||||
|
|
||||||
- (Boolean)isNalReferencePicture:(unsigned char)nalType
|
- (Boolean)isNalReferencePicture:(unsigned char)nalType
|
||||||
{
|
{
|
||||||
if (videoFormat == VIDEO_FORMAT_H264) {
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
return nalType == 0x65;
|
return nalType == 0x65;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -114,16 +114,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (Boolean)isNalPictureData:(unsigned char)nalType
|
|
||||||
{
|
|
||||||
if (videoFormat == VIDEO_FORMAT_H264) {
|
|
||||||
return !(nalType == AVC_NAL_TYPE_SPS || nalType == AVC_NAL_TYPE_PPS);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
return !(nalType == HEVC_NAL_TYPE_VPS || nalType == HEVC_NAL_TYPE_SPS || nalType == HEVC_NAL_TYPE_PPS);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
|
- (void)updateBufferForRange:(CMBlockBufferRef)existingBuffer data:(unsigned char *)data offset:(int)offset length:(int)nalLength
|
||||||
{
|
{
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
@@ -190,25 +180,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This function must free data
|
// This function must free data for bufferType == BUFFER_TYPE_PICDATA
|
||||||
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length
|
- (int)submitDecodeBuffer:(unsigned char *)data length:(int)length bufferType:(int)bufferType
|
||||||
{
|
{
|
||||||
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
|
unsigned char nalType = data[FRAME_START_PREFIX_SIZE];
|
||||||
OSStatus status;
|
OSStatus status;
|
||||||
|
|
||||||
// Check for previous decoder errors before doing anything
|
if (bufferType != BUFFER_TYPE_PICDATA) {
|
||||||
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
if (bufferType == BUFFER_TYPE_VPS) {
|
||||||
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
|
|
||||||
|
|
||||||
// Recreate the display layer
|
|
||||||
[self reinitializeDisplayLayer];
|
|
||||||
|
|
||||||
// Request an IDR frame to initialize the new decoder
|
|
||||||
return DR_NEED_IDR;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (![self isNalPictureData:nalType]) {
|
|
||||||
if (IS_NAL_VPS(nalType)) {
|
|
||||||
Log(LOG_I, @"Got VPS");
|
Log(LOG_I, @"Got VPS");
|
||||||
vpsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
vpsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||||
waitingForVps = false;
|
waitingForVps = false;
|
||||||
@@ -216,14 +195,14 @@
|
|||||||
// We got a new VPS so wait for a new SPS to match it
|
// We got a new VPS so wait for a new SPS to match it
|
||||||
waitingForSps = true;
|
waitingForSps = true;
|
||||||
}
|
}
|
||||||
else if (IS_NAL_SPS(nalType)) {
|
else if (bufferType == BUFFER_TYPE_SPS) {
|
||||||
Log(LOG_I, @"Got SPS");
|
Log(LOG_I, @"Got SPS");
|
||||||
spsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
spsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||||
waitingForSps = false;
|
waitingForSps = false;
|
||||||
|
|
||||||
// We got a new SPS so wait for a new PPS to match it
|
// We got a new SPS so wait for a new PPS to match it
|
||||||
waitingForPps = true;
|
waitingForPps = true;
|
||||||
} else if (IS_NAL_PPS(nalType)) {
|
} else if (bufferType == BUFFER_TYPE_PPS) {
|
||||||
Log(LOG_I, @"Got PPS");
|
Log(LOG_I, @"Got PPS");
|
||||||
ppsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
ppsData = [NSData dataWithBytes:&data[FRAME_START_PREFIX_SIZE] length:length - FRAME_START_PREFIX_SIZE];
|
||||||
waitingForPps = false;
|
waitingForPps = false;
|
||||||
@@ -231,7 +210,7 @@
|
|||||||
|
|
||||||
// See if we've got all the parameter sets we need for our video format
|
// See if we've got all the parameter sets we need for our video format
|
||||||
if ([self readyForPictureData]) {
|
if ([self readyForPictureData]) {
|
||||||
if (videoFormat == VIDEO_FORMAT_H264) {
|
if (videoFormat & VIDEO_FORMAT_MASK_H264) {
|
||||||
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
|
const uint8_t* const parameterSetPointers[] = { [spsData bytes], [ppsData bytes] };
|
||||||
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
|
const size_t parameterSetSizes[] = { [spsData length], [ppsData length] };
|
||||||
|
|
||||||
@@ -252,7 +231,12 @@
|
|||||||
const size_t parameterSetSizes[] = { [vpsData length], [spsData length], [ppsData length] };
|
const size_t parameterSetSizes[] = { [vpsData length], [spsData length], [ppsData length] };
|
||||||
|
|
||||||
Log(LOG_I, @"Constructing new HEVC format description");
|
Log(LOG_I, @"Constructing new HEVC format description");
|
||||||
|
|
||||||
|
#if TARGET_OS_IPHONE
|
||||||
if (@available(iOS 11.0, *)) {
|
if (@available(iOS 11.0, *)) {
|
||||||
|
#else
|
||||||
|
if (@available(macOS 10.13, *)) {
|
||||||
|
#endif
|
||||||
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
status = CMVideoFormatDescriptionCreateFromHEVCParameterSets(kCFAllocatorDefault,
|
||||||
3, /* count of parameter sets */
|
3, /* count of parameter sets */
|
||||||
parameterSetPointers,
|
parameterSetPointers,
|
||||||
@@ -273,8 +257,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free the data buffer
|
// Data is NOT to be freed here. It's a direct usage of the caller's buffer.
|
||||||
free(data);
|
|
||||||
|
|
||||||
// No frame data to submit for these NALUs
|
// No frame data to submit for these NALUs
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
@@ -283,7 +266,24 @@
|
|||||||
if (formatDesc == NULL) {
|
if (formatDesc == NULL) {
|
||||||
// Can't decode if we haven't gotten our parameter sets yet
|
// Can't decode if we haven't gotten our parameter sets yet
|
||||||
free(data);
|
free(data);
|
||||||
return DR_OK;
|
return DR_NEED_IDR;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for previous decoder errors before doing anything
|
||||||
|
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
|
||||||
|
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error);
|
||||||
|
|
||||||
|
// Recreate the display layer on the main thread.
|
||||||
|
// We need to use dispatch_sync() or we may miss
|
||||||
|
// some parameter sets while the layer is being
|
||||||
|
// recreated.
|
||||||
|
dispatch_sync(dispatch_get_main_queue(), ^{
|
||||||
|
[self reinitializeDisplayLayer];
|
||||||
|
});
|
||||||
|
|
||||||
|
// Request an IDR frame to initialize the new decoder
|
||||||
|
free(data);
|
||||||
|
return DR_NEED_IDR;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we're decoding actual frame data here
|
// Now we're decoding actual frame data here
|
||||||
@@ -347,11 +347,22 @@
|
|||||||
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
|
CFDictionarySetValue(dict, kCMSampleAttachmentKey_DependsOnOthers, kCFBooleanFalse);
|
||||||
}
|
}
|
||||||
|
|
||||||
[displayLayer enqueueSampleBuffer:sampleBuffer];
|
#if !TARGET_OS_IPHONE
|
||||||
|
_view.frameCount++;
|
||||||
|
#endif
|
||||||
|
|
||||||
// Dereference the buffers
|
// Enqueue video samples on the main thread
|
||||||
CFRelease(blockBuffer);
|
dispatch_async(dispatch_get_main_queue(), ^{
|
||||||
CFRelease(sampleBuffer);
|
// Enqueue the next frame
|
||||||
|
[self->displayLayer enqueueSampleBuffer:sampleBuffer];
|
||||||
|
|
||||||
|
// Ensure the layer is visible now
|
||||||
|
self->displayLayer.hidden = NO;
|
||||||
|
|
||||||
|
// Dereference the buffers
|
||||||
|
CFRelease(blockBuffer);
|
||||||
|
CFRelease(sampleBuffer);
|
||||||
|
});
|
||||||
|
|
||||||
return DR_OK;
|
return DR_OK;
|
||||||
}
|
}
|
||||||
|
|||||||
+90
-16
@@ -7,6 +7,9 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
#import "UIAppView.h"
|
#import "UIAppView.h"
|
||||||
|
#import "AppAssetManager.h"
|
||||||
|
|
||||||
|
static const float REFRESH_CYCLE = 2.0f;
|
||||||
|
|
||||||
@implementation UIAppView {
|
@implementation UIAppView {
|
||||||
TemporaryApp* _app;
|
TemporaryApp* _app;
|
||||||
@@ -31,22 +34,31 @@ static UIImage* noImage;
|
|||||||
noImage = [UIImage imageNamed:@"NoAppImage"];
|
noImage = [UIImage imageNamed:@"NoAppImage"];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
_appButton = [UIButton buttonWithType:UIButtonTypeSystem];
|
||||||
|
#else
|
||||||
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
|
||||||
|
#endif
|
||||||
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
|
[_appButton setBackgroundImage:noImage forState:UIControlStateNormal];
|
||||||
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||||
[_appButton sizeToFit];
|
[_appButton sizeToFit];
|
||||||
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
|
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||||
|
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
|
||||||
[self addSubview:_appButton];
|
[self addSubview:_appButton];
|
||||||
[self sizeToFit];
|
[self sizeToFit];
|
||||||
|
|
||||||
_appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
|
|
||||||
self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
|
|
||||||
|
|
||||||
// Rasterizing the cell layer increases rendering performance by quite a bit
|
// Rasterizing the cell layer increases rendering performance by quite a bit
|
||||||
self.layer.shouldRasterize = YES;
|
self.layer.shouldRasterize = YES;
|
||||||
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
self.layer.rasterizationScale = [UIScreen mainScreen].scale;
|
||||||
|
|
||||||
|
[self updateAppImage];
|
||||||
|
[self startUpdateLoop];
|
||||||
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,40 +72,64 @@ static UIImage* noImage;
|
|||||||
_appOverlay = nil;
|
_appOverlay = nil;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_appButton.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
|
||||||
|
self.frame = CGRectMake(0, 0, noImage.size.width, noImage.size.height);
|
||||||
|
|
||||||
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
if ([_app.id isEqualToString:_app.host.currentGame]) {
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
_appButton.layer.masksToBounds = NO;
|
||||||
|
|
||||||
|
_appButton.layer.shadowColor = [UIColor greenColor].CGColor;
|
||||||
|
_appButton.layer.shadowOffset = CGSizeMake(5,8);
|
||||||
|
_appButton.layer.shadowOpacity = 0.7;
|
||||||
|
#else
|
||||||
// Only create the app overlay if needed
|
// Only create the app overlay if needed
|
||||||
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
_appOverlay = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"Play"]];
|
||||||
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
_appOverlay.layer.shadowColor = [UIColor blackColor].CGColor;
|
||||||
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
_appOverlay.layer.shadowOffset = CGSizeMake(0, 0);
|
||||||
_appOverlay.layer.shadowOpacity = 1;
|
_appOverlay.layer.shadowOpacity = 1;
|
||||||
_appOverlay.layer.shadowRadius = 2.0;
|
_appOverlay.layer.shadowRadius = 2.0;
|
||||||
|
|
||||||
[self addSubview:_appOverlay];
|
[self addSubview:_appOverlay];
|
||||||
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
|
_appOverlay.frame = CGRectMake(0, 0, noImage.size.width / 2.f, noImage.size.height / 4.f);
|
||||||
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
|
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Improve no-app image detection
|
|
||||||
BOOL noAppImage = false;
|
BOOL noAppImage = false;
|
||||||
|
|
||||||
if (_app.image != nil) {
|
// First check the memory cache
|
||||||
// Load the decoded image from the cache
|
UIImage* appImage = [_artCache objectForKey:_app];
|
||||||
UIImage* appImage = [_artCache objectForKey:_app];
|
if (appImage == nil) {
|
||||||
if (appImage == nil) {
|
// Next try to load from the on disk cache
|
||||||
// Not cached; we have to decode this now
|
appImage = [UIImage imageWithContentsOfFile:[AppAssetManager boxArtPathForApp:_app]];
|
||||||
appImage = [UIImage imageWithData:_app.image];
|
if (appImage != nil) {
|
||||||
[_artCache setObject:appImage forKey:_app];
|
[_artCache setObject:appImage forKey:_app];
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if (appImage != nil) {
|
||||||
// This size of image might be blank image received from GameStream.
|
// This size of image might be blank image received from GameStream.
|
||||||
|
// TODO: Improve no-app image detection
|
||||||
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
|
if (!(appImage.size.width == 130.f && appImage.size.height == 180.f) && // GFE 2.0
|
||||||
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
|
!(appImage.size.width == 628.f && appImage.size.height == 888.f)) { // GFE 3.0
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
//custom image to do TvOS hover popup effect
|
||||||
|
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
|
||||||
|
imageView.userInteractionEnabled = YES;
|
||||||
|
imageView.adjustsImageWhenAncestorFocused = YES;
|
||||||
|
imageView.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
[_appButton addSubview:imageView];
|
||||||
|
|
||||||
|
_appButton.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
self.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
#else
|
||||||
_appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
|
_appButton.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
|
||||||
self.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
|
self.frame = CGRectMake(0, 0, appImage.size.width / 2, appImage.size.height / 2);
|
||||||
|
|
||||||
_appOverlay.frame = CGRectMake(0, 0, self.frame.size.width / 2.f, self.frame.size.height / 4.f);
|
_appOverlay.frame = CGRectMake(0, 0, self.frame.size.width / 2.f, self.frame.size.height / 4.f);
|
||||||
_appOverlay.layer.shadowRadius = 4.0;
|
_appOverlay.layer.shadowRadius = 4.0;
|
||||||
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
|
[_appOverlay setCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/6)];
|
||||||
|
#endif
|
||||||
[_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
|
[_appButton setBackgroundImage:appImage forState:UIControlStateNormal];
|
||||||
[self setNeedsDisplay];
|
[self setNeedsDisplay];
|
||||||
} else {
|
} else {
|
||||||
@@ -105,18 +141,56 @@ static UIImage* noImage;
|
|||||||
|
|
||||||
if (noAppImage) {
|
if (noAppImage) {
|
||||||
_appLabel = [[UILabel alloc] init];
|
_appLabel = [[UILabel alloc] init];
|
||||||
CGFloat padding = 4.f;
|
|
||||||
[_appLabel setFrame: CGRectMake(padding, padding, _appButton.frame.size.width - 2 * padding, _appButton.frame.size.height - 2 * padding)];
|
|
||||||
[_appLabel setTextColor:[UIColor whiteColor]];
|
[_appLabel setTextColor:[UIColor whiteColor]];
|
||||||
[_appLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:10.f]];
|
|
||||||
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
|
[_appLabel setBaselineAdjustment:UIBaselineAdjustmentAlignCenters];
|
||||||
[_appLabel setTextAlignment:NSTextAlignmentCenter];
|
[_appLabel setTextAlignment:NSTextAlignmentCenter];
|
||||||
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
|
[_appLabel setLineBreakMode:NSLineBreakByWordWrapping];
|
||||||
[_appLabel setNumberOfLines:0];
|
[_appLabel setNumberOfLines:0];
|
||||||
[_appLabel setText:_app.name];
|
[_appLabel setText:_app.name];
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
[_appLabel setFont:[UIFont systemFontOfSize:16]];
|
||||||
|
[_appLabel setAdjustsFontSizeToFitWidth:YES];
|
||||||
|
[_appLabel setFrame: CGRectMake(0, 0, 200, 265)];
|
||||||
|
//custom image to do TvOS hover popup effect
|
||||||
|
UIImageView *imageView = [[UIImageView alloc] initWithImage:appImage];
|
||||||
|
imageView.userInteractionEnabled = YES;
|
||||||
|
imageView.adjustsImageWhenAncestorFocused = YES;
|
||||||
|
imageView.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
UIGraphicsBeginImageContextWithOptions(_appLabel.frame.size, false, 0);
|
||||||
|
[imageView.layer renderInContext:(UIGraphicsGetCurrentContext())];
|
||||||
|
[_appLabel.layer renderInContext:(UIGraphicsGetCurrentContext())];
|
||||||
|
UIImage *imageWithText = UIGraphicsGetImageFromCurrentImageContext();
|
||||||
|
UIGraphicsEndImageContext();
|
||||||
|
[imageView setImage:imageWithText];
|
||||||
|
[_appButton addSubview:imageView];
|
||||||
|
_appButton.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
self.frame = CGRectMake(0, 0, 200, 265);
|
||||||
|
#else
|
||||||
|
CGFloat padding = 4.f;
|
||||||
|
[_appLabel setFrame: CGRectMake(padding, padding, _appButton.frame.size.width - 2 * padding, _appButton.frame.size.height - 2 * padding)];
|
||||||
[_appButton addSubview:_appLabel];
|
[_appButton addSubview:_appLabel];
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
- (void) startUpdateLoop {
|
||||||
|
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||||
|
}
|
||||||
|
|
||||||
|
- (void) updateLoop {
|
||||||
|
// Update the app image if neccessary
|
||||||
|
if ((_appOverlay != nil && ![_app.id isEqualToString:_app.host.currentGame]) ||
|
||||||
|
(_appOverlay == nil && [_app.id isEqualToString:_app.host.currentGame])) {
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
[self updateAppImage];
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop updating when we detach from our parent view
|
||||||
|
if (self.superview != nil) {
|
||||||
|
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -18,7 +18,12 @@
|
|||||||
CGSize _labelSize;
|
CGSize _labelSize;
|
||||||
}
|
}
|
||||||
static const float REFRESH_CYCLE = 2.0f;
|
static const float REFRESH_CYCLE = 2.0f;
|
||||||
|
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
static const int LABEL_DY = 40;
|
||||||
|
#else
|
||||||
static const int LABEL_DY = 20;
|
static const int LABEL_DY = 20;
|
||||||
|
#endif
|
||||||
|
|
||||||
- (id) init {
|
- (id) init {
|
||||||
self = [super init];
|
self = [super init];
|
||||||
@@ -34,9 +39,7 @@ static const int LABEL_DY = 20;
|
|||||||
_hostLabel = [[UILabel alloc] init];
|
_hostLabel = [[UILabel alloc] init];
|
||||||
_hostStatus = [[UILabel alloc] init];
|
_hostStatus = [[UILabel alloc] init];
|
||||||
_hostPairState = [[UILabel alloc] init];
|
_hostPairState = [[UILabel alloc] init];
|
||||||
[_hostLabel setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
|
|
||||||
[_hostStatus setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
|
|
||||||
[_hostPairState setFont:[UIFont fontWithName:@"Roboto-Regular" size:[UIFont systemFontSize]]];
|
|
||||||
return self;
|
return self;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,12 +49,19 @@ static const int LABEL_DY = 20;
|
|||||||
|
|
||||||
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
|
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
|
||||||
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
|
||||||
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
|
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||||
|
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
[_hostButton sizeToFit];
|
[_hostButton sizeToFit];
|
||||||
|
|
||||||
[_hostLabel setText:@"Add Host"];
|
[_hostLabel setText:@"Add Host"];
|
||||||
[_hostLabel sizeToFit];
|
[_hostLabel sizeToFit];
|
||||||
|
#if !TARGET_OS_TV
|
||||||
_hostLabel.textColor = [UIColor whiteColor];
|
_hostLabel.textColor = [UIColor whiteColor];
|
||||||
|
#endif
|
||||||
_hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
|
_hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
|
||||||
|
|
||||||
UIImageView* addIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AddComputerIcon"]];
|
UIImageView* addIcon = [[UIImageView alloc] initWithImage:[UIImage imageNamed:@"AddComputerIcon"]];
|
||||||
@@ -77,9 +87,15 @@ static const int LABEL_DY = 20;
|
|||||||
_host = host;
|
_host = host;
|
||||||
_callback = callback;
|
_callback = callback;
|
||||||
|
|
||||||
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked)];
|
UILongPressGestureRecognizer* longPressRecognizer = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(hostLongClicked:)];
|
||||||
[_hostButton addGestureRecognizer:longPressRecognizer];
|
[_hostButton addGestureRecognizer:longPressRecognizer];
|
||||||
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
|
|
||||||
|
if (@available(iOS 9.0, tvOS 9.0, *)) {
|
||||||
|
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventPrimaryActionTriggered];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
|
||||||
|
}
|
||||||
|
|
||||||
[self updateContentsForHost:host];
|
[self updateContentsForHost:host];
|
||||||
[self updateBounds];
|
[self updateBounds];
|
||||||
@@ -125,7 +141,9 @@ static const int LABEL_DY = 20;
|
|||||||
|
|
||||||
- (void) updateContentsForHost:(TemporaryHost*)host {
|
- (void) updateContentsForHost:(TemporaryHost*)host {
|
||||||
_hostLabel.text = _host.name;
|
_hostLabel.text = _host.name;
|
||||||
|
#if !TARGET_OS_TV
|
||||||
_hostLabel.textColor = [UIColor whiteColor];
|
_hostLabel.textColor = [UIColor whiteColor];
|
||||||
|
#endif
|
||||||
[_hostLabel sizeToFit];
|
[_hostLabel sizeToFit];
|
||||||
|
|
||||||
switch (host.pairState) {
|
switch (host.pairState) {
|
||||||
@@ -139,7 +157,9 @@ static const int LABEL_DY = 20;
|
|||||||
_hostPairState.text = @"Paired";
|
_hostPairState.text = @"Paired";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
#if !TARGET_OS_TV
|
||||||
_hostPairState.textColor = [UIColor whiteColor];
|
_hostPairState.textColor = [UIColor whiteColor];
|
||||||
|
#endif
|
||||||
[_hostPairState sizeToFit];
|
[_hostPairState sizeToFit];
|
||||||
|
|
||||||
if (host.online) {
|
if (host.online) {
|
||||||
@@ -163,11 +183,17 @@ static const int LABEL_DY = 20;
|
|||||||
|
|
||||||
- (void) updateLoop {
|
- (void) updateLoop {
|
||||||
[self updateContentsForHost:_host];
|
[self updateContentsForHost:_host];
|
||||||
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
|
||||||
|
// Stop updating when we detach from our parent view
|
||||||
|
if (self.superview != nil) {
|
||||||
|
[self performSelector:@selector(updateLoop) withObject:self afterDelay:REFRESH_CYCLE];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) hostLongClicked {
|
- (void) hostLongClicked:(UILongPressGestureRecognizer*)gesture {
|
||||||
[_callback hostLongClicked:_host view:self];
|
if (gesture.state == UIGestureRecognizerStateBegan) {
|
||||||
|
[_callback hostLongClicked:_host view:self];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) hostClicked {
|
- (void) hostClicked {
|
||||||
@@ -178,4 +204,14 @@ static const int LABEL_DY = 20;
|
|||||||
[_callback addHostClicked];
|
[_callback addHostClicked];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#if TARGET_OS_TV
|
||||||
|
- (void) didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
|
||||||
|
UIButton *previousButton = (UIButton *)context.previouslyFocusedItem;
|
||||||
|
UIButton *nextButton = (UIButton *) context.nextFocusedItem;
|
||||||
|
|
||||||
|
previousButton.layer.shadowColor = [[UIColor blackColor] CGColor];
|
||||||
|
nextButton.layer.shadowColor = [[UIColor greenColor] CGColor];
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
#import "Logger.h"
|
#import "Logger.h"
|
||||||
|
|
||||||
|
static LogLevel LoggerLogLevel = LOG_I;
|
||||||
|
|
||||||
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args);
|
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args);
|
||||||
|
|
||||||
void Log(LogLevel level, NSString* fmt, ...) {
|
void Log(LogLevel level, NSString* fmt, ...) {
|
||||||
@@ -27,6 +29,10 @@ void LogTag(LogLevel level, NSString* tag, NSString* fmt, ...) {
|
|||||||
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
|
void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
|
||||||
NSString* levelPrefix = @"";
|
NSString* levelPrefix = @"";
|
||||||
|
|
||||||
|
if (level < LoggerLogLevel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
switch(level) {
|
switch(level) {
|
||||||
case LOG_D:
|
case LOG_D:
|
||||||
levelPrefix = PRFX_DEBUG;
|
levelPrefix = PRFX_DEBUG;
|
||||||
@@ -41,6 +47,8 @@ void LogTagv(LogLevel level, NSString* tag, NSString* fmt, va_list args) {
|
|||||||
levelPrefix = PRFX_ERROR;
|
levelPrefix = PRFX_ERROR;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
|
levelPrefix = @"";
|
||||||
|
assert(false);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
NSString* prefixedString;
|
NSString* prefixedString;
|
||||||
|
|||||||
@@ -6,8 +6,6 @@
|
|||||||
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
#import <Foundation/Foundation.h>
|
|
||||||
|
|
||||||
@interface Utils : NSObject
|
@interface Utils : NSObject
|
||||||
|
|
||||||
typedef NS_ENUM(int, PairState) {
|
typedef NS_ENUM(int, PairState) {
|
||||||
@@ -22,6 +20,7 @@ FOUNDATION_EXPORT NSString *const deviceName;
|
|||||||
+ (NSString*) bytesToHex:(NSData*)data;
|
+ (NSString*) bytesToHex:(NSData*)data;
|
||||||
+ (NSData*) hexToBytes:(NSString*) hex;
|
+ (NSData*) hexToBytes:(NSString*) hex;
|
||||||
+ (int) resolveHost:(NSString*)host;
|
+ (int) resolveHost:(NSString*)host;
|
||||||
|
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog;
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,15 @@ NSString *const deviceName = @"roth";
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
+ (void) addHelpOptionToDialog:(UIAlertController*)dialog {
|
||||||
|
#if !TARGET_OS_TV
|
||||||
|
// tvOS doesn't have a browser
|
||||||
|
[dialog addAction:[UIAlertAction actionWithTitle:@"Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||||
|
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
|
||||||
|
}]];
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
@end
|
@end
|
||||||
|
|
||||||
@implementation NSString (NSStringWithTrim)
|
@implementation NSString (NSStringWithTrim)
|
||||||
|
|||||||
@@ -10,5 +10,11 @@
|
|||||||
|
|
||||||
@interface LoadingFrameViewController : UIViewController
|
@interface LoadingFrameViewController : UIViewController
|
||||||
|
|
||||||
|
- (void)showLoadingFrame:(void (^)(void))completion;
|
||||||
|
|
||||||
|
- (void)dismissLoadingFrame:(void (^)(void))completion;
|
||||||
|
|
||||||
|
- (BOOL)isShown;
|
||||||
|
|
||||||
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingSpinner;
|
@property (weak, nonatomic) IBOutlet UIActivityIndicatorView *loadingSpinner;
|
||||||
@end
|
@end
|
||||||
@@ -8,11 +8,9 @@
|
|||||||
|
|
||||||
#import "LoadingFrameViewController.h"
|
#import "LoadingFrameViewController.h"
|
||||||
|
|
||||||
@interface LoadingFrameViewController ()
|
@implementation LoadingFrameViewController {
|
||||||
|
BOOL presented;
|
||||||
@end
|
};
|
||||||
|
|
||||||
@implementation LoadingFrameViewController
|
|
||||||
|
|
||||||
- (void)viewDidLoad {
|
- (void)viewDidLoad {
|
||||||
[super viewDidLoad];
|
[super viewDidLoad];
|
||||||
@@ -20,19 +18,66 @@
|
|||||||
self.loadingSpinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
self.loadingSpinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didReceiveMemoryWarning {
|
- (UIViewController*) activeViewController {
|
||||||
[super didReceiveMemoryWarning];
|
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||||
// Dispose of any resources that can be recreated.
|
|
||||||
|
while (topController.presentedViewController) {
|
||||||
|
topController = topController.presentedViewController;
|
||||||
|
}
|
||||||
|
|
||||||
|
return topController;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
- (void)showLoadingFrame:(void (^)(void))completion {
|
||||||
#pragma mark - Navigation
|
if (!presented) {
|
||||||
|
Log(LOG_I, @"Loading frame presenting start");
|
||||||
|
presented = YES;
|
||||||
|
[[self activeViewController] presentViewController:self animated:NO completion:^{
|
||||||
|
Log(LOG_I, @"Loading frame presenting complete");
|
||||||
|
if (completion) {
|
||||||
|
completion();
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
else if (completion) {
|
||||||
|
Log(LOG_E, @"Loading frame already shown!");
|
||||||
|
completion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// In a storyboard-based application, you will often want to do a little preparation before navigation
|
- (void)dismissLoadingFrame:(void (^)(void))completion {
|
||||||
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
if (presented) {
|
||||||
// Get the new view controller using [segue destinationViewController].
|
Log(LOG_I, @"Loading frame hiding start");
|
||||||
// Pass the selected object to the new view controller.
|
[self dismissViewControllerAnimated:NO completion:^{
|
||||||
|
Log(LOG_I, @"Loading frame hiding complete");
|
||||||
|
|
||||||
|
// Since presented is set to NO here rather than
|
||||||
|
// immediately in dismissLoadingFrame, we may
|
||||||
|
// falsely avoid displaying the loading frame if
|
||||||
|
// a dismiss is in progress while attempting to show
|
||||||
|
// the frame. That's preferable to crashing due to
|
||||||
|
// displaying the same VC twice though.
|
||||||
|
//
|
||||||
|
// This scenario can happen if the app is suspended
|
||||||
|
// while the dismiss is in progress then on resume
|
||||||
|
// it attempts to display it again before the dismiss
|
||||||
|
// completes. It can be reproduced by rapidly pressing
|
||||||
|
// Home and switching back to Moonlight while in the app grid.
|
||||||
|
// It reproduces more easily if the VC transitions are animated.
|
||||||
|
self->presented = NO;
|
||||||
|
|
||||||
|
if (completion) {
|
||||||
|
completion();
|
||||||
|
}
|
||||||
|
}];
|
||||||
|
}
|
||||||
|
else if (completion) {
|
||||||
|
completion();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
- (BOOL)isShown {
|
||||||
|
return presented;
|
||||||
}
|
}
|
||||||
*/
|
|
||||||
|
|
||||||
@end
|
@end
|
||||||
@@ -17,7 +17,9 @@
|
|||||||
|
|
||||||
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
|
@interface MainFrameViewController : UICollectionViewController <DiscoveryCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate, SWRevealViewControllerDelegate>
|
||||||
|
|
||||||
|
#if !TARGET_OS_TV
|
||||||
@property (strong, nonatomic) IBOutlet UIButton *limelightLogoButton;
|
@property (strong, nonatomic) IBOutlet UIButton *limelightLogoButton;
|
||||||
@property (weak, nonatomic) IBOutlet UIBarButtonItem *computerNameButton;
|
@property (weak, nonatomic) IBOutlet UIBarButtonItem *computerNameButton;
|
||||||
|
#endif
|
||||||
|
|
||||||
@end
|
@end
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -996,16 +996,32 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
|
|
||||||
- (void)_getRevealWidth:(CGFloat*)pRevealWidth revealOverDraw:(CGFloat*)pRevealOverdraw forSymetry:(int)symetry
|
- (void)_getRevealWidth:(CGFloat*)pRevealWidth revealOverDraw:(CGFloat*)pRevealOverdraw forSymetry:(int)symetry
|
||||||
{
|
{
|
||||||
if ( symetry < 0 ) *pRevealWidth = _rightViewRevealWidth, *pRevealOverdraw = _rightViewRevealOverdraw;
|
if ( symetry < 0 )
|
||||||
else *pRevealWidth = _rearViewRevealWidth, *pRevealOverdraw = _rearViewRevealOverdraw;
|
{
|
||||||
|
*pRevealWidth = _rightViewRevealWidth;
|
||||||
|
*pRevealOverdraw = _rightViewRevealOverdraw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*pRevealWidth = _rearViewRevealWidth;
|
||||||
|
*pRevealOverdraw = _rearViewRevealOverdraw;
|
||||||
|
}
|
||||||
|
|
||||||
if (*pRevealWidth < 0) *pRevealWidth = _contentView.bounds.size.width + *pRevealWidth;
|
if (*pRevealWidth < 0) *pRevealWidth = _contentView.bounds.size.width + *pRevealWidth;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)_getBounceBack:(BOOL*)pBounceBack pStableDrag:(BOOL*)pStableDrag forSymetry:(int)symetry
|
- (void)_getBounceBack:(BOOL*)pBounceBack pStableDrag:(BOOL*)pStableDrag forSymetry:(int)symetry
|
||||||
{
|
{
|
||||||
if ( symetry < 0 ) *pBounceBack = _bounceBackOnLeftOverdraw, *pStableDrag = _stableDragOnLeftOverdraw;
|
if ( symetry < 0 )
|
||||||
else *pBounceBack = _bounceBackOnOverdraw, *pStableDrag = _stableDragOnOverdraw;
|
{
|
||||||
|
*pBounceBack = _bounceBackOnLeftOverdraw;
|
||||||
|
*pStableDrag = _stableDragOnLeftOverdraw;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
*pBounceBack = _bounceBackOnOverdraw;
|
||||||
|
*pStableDrag = _stableDragOnOverdraw;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)_getAdjustedFrontViewPosition:(FrontViewPosition*)frontViewPosition forSymetry:(int)symetry
|
- (void)_getAdjustedFrontViewPosition:(FrontViewPosition*)frontViewPosition forSymetry:(int)symetry
|
||||||
@@ -1371,8 +1387,15 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
|
|
||||||
// These are derived from the primary view controller
|
// These are derived from the primary view controller
|
||||||
if (@available(iOS 11.0, *)) {
|
if (@available(iOS 11.0, *)) {
|
||||||
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
// iOS 11 betas (which are still installed in some places :\) crash with
|
||||||
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
|
// doesNotRecognizeSelector since this wasn't added until after the iPhone X
|
||||||
|
// announcement.
|
||||||
|
if ([self respondsToSelector: @selector(setNeedsUpdateOfHomeIndicatorAutoHidden)]) {
|
||||||
|
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
|
||||||
|
}
|
||||||
|
if ([self respondsToSelector: @selector(setNeedsUpdateOfScreenEdgesDeferringSystemGestures)]) {
|
||||||
|
[self setNeedsUpdateOfScreenEdgesDeferringSystemGestures];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1391,21 +1414,21 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
// Primitive method for view controller deployment and animated layout to the given position.
|
// Primitive method for view controller deployment and animated layout to the given position.
|
||||||
- (void)_setFrontViewPosition:(FrontViewPosition)newPosition withDuration:(NSTimeInterval)duration
|
- (void)_setFrontViewPosition:(FrontViewPosition)newPosition withDuration:(NSTimeInterval)duration
|
||||||
{
|
{
|
||||||
void (^rearDeploymentCompletion)() = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
|
void (^rearDeploymentCompletion)(void) = [self _rearViewDeploymentForNewFrontViewPosition:newPosition];
|
||||||
void (^rightDeploymentCompletion)() = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
|
void (^rightDeploymentCompletion)(void) = [self _rightViewDeploymentForNewFrontViewPosition:newPosition];
|
||||||
void (^frontDeploymentCompletion)() = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
|
void (^frontDeploymentCompletion)(void) = [self _frontViewDeploymentForNewFrontViewPosition:newPosition];
|
||||||
|
|
||||||
void (^animations)() = ^()
|
void (^animations)(void) = ^(void)
|
||||||
{
|
{
|
||||||
// Calling this in the animation block causes the status bar to appear/dissapear in sync with our own animation
|
// Calling this in the animation block causes the status bar to appear/dissapear in sync with our own animation
|
||||||
[self setNeedsStatusBarAppearanceUpdate];
|
[self setNeedsStatusBarAppearanceUpdate];
|
||||||
|
|
||||||
// We call the layoutSubviews method on the contentView view and send a delegate, which will
|
// We call the layoutSubviews method on the contentView view and send a delegate, which will
|
||||||
// occur inside of an animation block if any animated transition is being performed
|
// occur inside of an animation block if any animated transition is being performed
|
||||||
[_contentView layoutSubviews];
|
[self->_contentView layoutSubviews];
|
||||||
|
|
||||||
if ([_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
|
if ([self->_delegate respondsToSelector:@selector(revealController:animateToPosition:)])
|
||||||
[_delegate revealController:self animateToPosition:_frontViewPosition];
|
[self->_delegate revealController:self animateToPosition:self->_frontViewPosition];
|
||||||
};
|
};
|
||||||
|
|
||||||
void (^completion)(BOOL) = ^(BOOL finished)
|
void (^completion)(BOOL) = ^(BOOL finished)
|
||||||
@@ -1448,21 +1471,31 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
UIView *view = nil;
|
UIView *view = nil;
|
||||||
|
|
||||||
if ( operation == SWRevealControllerOperationReplaceRearController )
|
if ( operation == SWRevealControllerOperationReplaceRearController )
|
||||||
old = _rearViewController, _rearViewController = new, view = _contentView.rearView;
|
{
|
||||||
|
old = _rearViewController;
|
||||||
|
_rearViewController = new;
|
||||||
|
view = _contentView.rearView;
|
||||||
|
}
|
||||||
else if ( operation == SWRevealControllerOperationReplaceFrontController )
|
else if ( operation == SWRevealControllerOperationReplaceFrontController )
|
||||||
old = _frontViewController, _frontViewController = new, view = _contentView.frontView;
|
{
|
||||||
|
old = _frontViewController;
|
||||||
|
_frontViewController = new;
|
||||||
|
view = _contentView.frontView;
|
||||||
|
}
|
||||||
else if ( operation == SWRevealControllerOperationReplaceRightController )
|
else if ( operation == SWRevealControllerOperationReplaceRightController )
|
||||||
old = _rightViewController, _rightViewController = new, view = _contentView.rightView;
|
{
|
||||||
|
old = _rightViewController;
|
||||||
|
_rightViewController = new;
|
||||||
|
view = _contentView.rightView;
|
||||||
|
}
|
||||||
|
|
||||||
void (^completion)() = [self _transitionFromViewController:old toViewController:new inView:view];
|
void (^completion)(void) = [self _transitionFromViewController:old toViewController:new inView:view];
|
||||||
|
|
||||||
void (^animationCompletion)() = ^
|
void (^animationCompletion)(void) = ^
|
||||||
{
|
{
|
||||||
completion();
|
completion();
|
||||||
if ( [_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
|
if ( [self->_delegate respondsToSelector:@selector(revealController:didAddViewController:forOperation:animated:)] )
|
||||||
[_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
|
[self->_delegate revealController:self didAddViewController:new forOperation:operation animated:animated];
|
||||||
|
|
||||||
[self _dequeue];
|
[self _dequeue];
|
||||||
};
|
};
|
||||||
@@ -1520,16 +1553,16 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
|
|
||||||
_frontViewPosition = newPosition;
|
_frontViewPosition = newPosition;
|
||||||
|
|
||||||
void (^deploymentCompletion)() =
|
void (^deploymentCompletion)(void) =
|
||||||
[self _deploymentForViewController:_frontViewController inView:_contentView.frontView appear:appear disappear:disappear];
|
[self _deploymentForViewController:_frontViewController inView:_contentView.frontView appear:appear disappear:disappear];
|
||||||
|
|
||||||
void (^completion)() = ^()
|
void (^completion)(void) = ^(void)
|
||||||
{
|
{
|
||||||
deploymentCompletion();
|
deploymentCompletion();
|
||||||
if ( positionIsChanging )
|
if ( positionIsChanging )
|
||||||
{
|
{
|
||||||
if ( [_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
|
if ( [self->_delegate respondsToSelector:@selector(revealController:didMoveToPosition:)] )
|
||||||
[_delegate revealController:self didMoveToPosition:newPosition];
|
[self->_delegate revealController:self didMoveToPosition:newPosition];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1645,11 +1678,11 @@ const int FrontViewPositionNone = 0xff;
|
|||||||
|
|
||||||
if ( toController ) [self addChildViewController:toController];
|
if ( toController ) [self addChildViewController:toController];
|
||||||
|
|
||||||
void (^deployCompletion)() = [self _deployForViewController:toController inView:view];
|
void (^deployCompletion)(void) = [self _deployForViewController:toController inView:view];
|
||||||
|
|
||||||
[fromController willMoveToParentViewController:nil];
|
[fromController willMoveToParentViewController:nil];
|
||||||
|
|
||||||
void (^undeployCompletion)() = [self _undeployForViewController:fromController];
|
void (^undeployCompletion)(void) = [self _undeployForViewController:fromController];
|
||||||
|
|
||||||
void (^completionBlock)(void) = ^(void)
|
void (^completionBlock)(void) = ^(void)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *framerateSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *resolutionSelector;
|
||||||
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *onscreenControlSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *optimizeSettingsSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *multiControllerSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UISegmentedControl *hevcSelector;
|
||||||
|
@property (strong, nonatomic) IBOutlet UIScrollView *scrollView;
|
||||||
|
|
||||||
- (void) saveSettings;
|
- (void) saveSettings;
|
||||||
|
|
||||||
|
|||||||
@@ -10,17 +10,78 @@
|
|||||||
#import "TemporarySettings.h"
|
#import "TemporarySettings.h"
|
||||||
#import "DataManager.h"
|
#import "DataManager.h"
|
||||||
|
|
||||||
#define BITRATE_INTERVAL 500 // in kbps
|
|
||||||
|
|
||||||
@implementation SettingsViewController {
|
@implementation SettingsViewController {
|
||||||
NSInteger _bitrate;
|
NSInteger _bitrate;
|
||||||
Boolean _adjustedForSafeArea;
|
Boolean _adjustedForSafeArea;
|
||||||
}
|
}
|
||||||
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
||||||
|
static const int bitrateTable[] = {
|
||||||
|
500,
|
||||||
|
1000,
|
||||||
|
1500,
|
||||||
|
2000,
|
||||||
|
2500,
|
||||||
|
3000,
|
||||||
|
4000,
|
||||||
|
5000,
|
||||||
|
6000,
|
||||||
|
7000,
|
||||||
|
8000,
|
||||||
|
9000,
|
||||||
|
10000,
|
||||||
|
12000,
|
||||||
|
15000,
|
||||||
|
18000,
|
||||||
|
20000,
|
||||||
|
30000,
|
||||||
|
40000,
|
||||||
|
50000,
|
||||||
|
60000,
|
||||||
|
70000,
|
||||||
|
80000,
|
||||||
|
100000
|
||||||
|
};
|
||||||
|
|
||||||
- (void)viewDidAppear:(BOOL)animated
|
-(int)getSliderValueForBitrate:(NSInteger)bitrate {
|
||||||
{
|
int i;
|
||||||
[super viewDidAppear:animated];
|
|
||||||
|
for (i = 0; i < (sizeof(bitrateTable) / sizeof(*bitrateTable)); i++) {
|
||||||
|
if (bitrate <= bitrateTable[i]) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return the last entry in the table
|
||||||
|
return i - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
-(void)viewDidLayoutSubviews {
|
||||||
|
// On iPhone layouts, this view is rooted at a ScrollView. To make it
|
||||||
|
// scrollable, we'll update content size here.
|
||||||
|
if (self.scrollView != nil) {
|
||||||
|
CGFloat highestViewY = 0;
|
||||||
|
|
||||||
|
// Enumerate the scroll view's subviews looking for the
|
||||||
|
// highest view Y value to set our scroll view's content
|
||||||
|
// size.
|
||||||
|
for (UIView* view in self.scrollView.subviews) {
|
||||||
|
// UIScrollViews have 2 default UIImageView children
|
||||||
|
// which represent the horizontal and vertical scrolling
|
||||||
|
// indicators. Ignore these when computing content size.
|
||||||
|
if ([view isKindOfClass:[UIImageView class]]) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
CGFloat currentViewY = view.frame.origin.y + view.frame.size.height;
|
||||||
|
if (currentViewY > highestViewY) {
|
||||||
|
highestViewY = currentViewY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a bit of padding so the view doesn't end right at the button of the display
|
||||||
|
self.scrollView.contentSize = CGSizeMake(self.scrollView.contentSize.width,
|
||||||
|
highestViewY + 20);
|
||||||
|
}
|
||||||
|
|
||||||
// Adjust the subviews for the safe area on the iPhone X.
|
// Adjust the subviews for the safe area on the iPhone X.
|
||||||
if (!_adjustedForSafeArea) {
|
if (!_adjustedForSafeArea) {
|
||||||
@@ -45,55 +106,106 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
|||||||
DataManager* dataMan = [[DataManager alloc] init];
|
DataManager* dataMan = [[DataManager alloc] init];
|
||||||
TemporarySettings* currentSettings = [dataMan getSettings];
|
TemporarySettings* currentSettings = [dataMan getSettings];
|
||||||
|
|
||||||
// Bitrate is persisted in kbps
|
// Ensure we pick a bitrate that falls exactly onto a slider notch
|
||||||
_bitrate = [currentSettings.bitrate integerValue];
|
_bitrate = bitrateTable[[self getSliderValueForBitrate:[currentSettings.bitrate intValue]]];
|
||||||
NSInteger framerate = [currentSettings.framerate integerValue] == 30 ? 0 : 1;
|
|
||||||
NSInteger resolution;
|
|
||||||
if ([currentSettings.height integerValue] == 720) {
|
|
||||||
resolution = 0;
|
|
||||||
} else if ([currentSettings.height integerValue] == 1080) {
|
|
||||||
resolution = 1;
|
|
||||||
} else {
|
|
||||||
resolution = 0;
|
|
||||||
}
|
|
||||||
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
|
|
||||||
|
|
||||||
|
NSInteger framerate;
|
||||||
|
switch ([currentSettings.framerate integerValue]) {
|
||||||
|
case 30:
|
||||||
|
framerate = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 60:
|
||||||
|
framerate = 1;
|
||||||
|
break;
|
||||||
|
case 120:
|
||||||
|
framerate = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
NSInteger resolution;
|
||||||
|
switch ([currentSettings.height integerValue]) {
|
||||||
|
case 360:
|
||||||
|
resolution = 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
case 720:
|
||||||
|
resolution = 1;
|
||||||
|
break;
|
||||||
|
case 1080:
|
||||||
|
resolution = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only show the 120 FPS option if we have a > 60-ish Hz display
|
||||||
|
bool enable120Fps = false;
|
||||||
|
if (@available(iOS 10.3, *)) {
|
||||||
|
if ([UIScreen mainScreen].maximumFramesPerSecond > 62) {
|
||||||
|
enable120Fps = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!enable120Fps) {
|
||||||
|
[self.framerateSelector removeSegmentAtIndex:2 animated:NO];
|
||||||
|
}
|
||||||
|
|
||||||
|
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
|
||||||
|
[self.multiControllerSelector setSelectedSegmentIndex:currentSettings.multiController ? 1 : 0];
|
||||||
|
[self.audioOnPCSelector setSelectedSegmentIndex:currentSettings.playAudioOnPC ? 1 : 0];
|
||||||
|
[self.hevcSelector setSelectedSegmentIndex:currentSettings.useHevc ? 1 : 0];
|
||||||
|
NSInteger onscreenControls = [currentSettings.onscreenControls integerValue];
|
||||||
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
[self.resolutionSelector setSelectedSegmentIndex:resolution];
|
||||||
[self.resolutionSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
|
[self.resolutionSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
|
||||||
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
[self.framerateSelector setSelectedSegmentIndex:framerate];
|
||||||
[self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
|
[self.framerateSelector addTarget:self action:@selector(newResolutionFpsChosen) forControlEvents:UIControlEventValueChanged];
|
||||||
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
|
[self.onscreenControlSelector setSelectedSegmentIndex:onscreenControls];
|
||||||
[self.bitrateSlider setValue:(_bitrate / BITRATE_INTERVAL) animated:YES];
|
[self.bitrateSlider setMinimumValue:0];
|
||||||
|
[self.bitrateSlider setMaximumValue:(sizeof(bitrateTable) / sizeof(*bitrateTable)) - 1];
|
||||||
|
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||||
[self updateBitrateText];
|
[self updateBitrateText];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) newResolutionFpsChosen {
|
- (void) newResolutionFpsChosen {
|
||||||
NSInteger frameRate = [self getChosenFrameRate];
|
NSInteger fps = [self getChosenFrameRate];
|
||||||
NSInteger resHeight = [self getChosenStreamHeight];
|
NSInteger width = [self getChosenStreamWidth];
|
||||||
|
NSInteger height = [self getChosenStreamHeight];
|
||||||
NSInteger defaultBitrate;
|
NSInteger defaultBitrate;
|
||||||
|
|
||||||
// 1080p60 is 20 Mbps
|
// This table prefers 16:10 resolutions because they are
|
||||||
if (frameRate == 60 && resHeight == 1080) {
|
// only slightly more pixels than the 16:9 equivalents, so
|
||||||
defaultBitrate = 20000;
|
// we don't want to bump those 16:10 resolutions up to the
|
||||||
|
// next 16:9 slot.
|
||||||
|
//
|
||||||
|
// This logic is shamelessly stolen from Moonlight Qt:
|
||||||
|
// https://github.com/moonlight-stream/moonlight-qt/blob/master/app/settings/streamingpreferences.cpp
|
||||||
|
|
||||||
|
if (width * height <= 640 * 360) {
|
||||||
|
defaultBitrate = 1000 * (fps / 30.0);
|
||||||
}
|
}
|
||||||
// 720p60 and 1080p30 are 10 Mbps
|
// This covers 1280x720 and 1280x800 too
|
||||||
else if (frameRate == 60 || resHeight == 1080) {
|
else if (width * height <= 1366 * 768) {
|
||||||
defaultBitrate = 10000;
|
defaultBitrate = 5000 * (fps / 30.0);
|
||||||
}
|
}
|
||||||
// 720p30 is 5 Mbps
|
else if (width * height <= 1920 * 1200) {
|
||||||
else {
|
defaultBitrate = 10000 * (fps / 30.0);
|
||||||
defaultBitrate = 5000;
|
}
|
||||||
|
else if (width * height <= 2560 * 1600) {
|
||||||
|
defaultBitrate = 20000 * (fps / 30.0);
|
||||||
|
}
|
||||||
|
else /* if (width * height <= 3840 * 2160) */ {
|
||||||
|
defaultBitrate = 40000 * (fps / 30.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We should always be exactly on a slider position with default bitrates
|
||||||
_bitrate = defaultBitrate;
|
_bitrate = defaultBitrate;
|
||||||
[self.bitrateSlider setValue:defaultBitrate / BITRATE_INTERVAL animated:YES];
|
assert(bitrateTable[[self getSliderValueForBitrate:_bitrate]] == _bitrate);
|
||||||
|
[self.bitrateSlider setValue:[self getSliderValueForBitrate:_bitrate] animated:YES];
|
||||||
|
|
||||||
[self updateBitrateText];
|
[self updateBitrateText];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) bitrateSliderMoved {
|
- (void) bitrateSliderMoved {
|
||||||
_bitrate = BITRATE_INTERVAL * (int)self.bitrateSlider.value;
|
assert(self.bitrateSlider.value < (sizeof(bitrateTable) / sizeof(*bitrateTable)));
|
||||||
|
_bitrate = bitrateTable[(int)self.bitrateSlider.value];
|
||||||
[self updateBitrateText];
|
[self updateBitrateText];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,15 +215,26 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
|||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger) getChosenFrameRate {
|
- (NSInteger) getChosenFrameRate {
|
||||||
return [self.framerateSelector selectedSegmentIndex] == 0 ? 30 : 60;
|
switch ([self.framerateSelector selectedSegmentIndex]) {
|
||||||
|
case 0:
|
||||||
|
return 30;
|
||||||
|
case 1:
|
||||||
|
return 60;
|
||||||
|
case 2:
|
||||||
|
return 120;
|
||||||
|
default:
|
||||||
|
abort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger) getChosenStreamHeight {
|
- (NSInteger) getChosenStreamHeight {
|
||||||
return [self.resolutionSelector selectedSegmentIndex] == 0 ? 720 : 1080;
|
const int resolutionTable[] = { 360, 720, 1080 };
|
||||||
|
return resolutionTable[[self.resolutionSelector selectedSegmentIndex]];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (NSInteger) getChosenStreamWidth {
|
- (NSInteger) getChosenStreamWidth {
|
||||||
return [self getChosenStreamHeight] == 720 ? 1280 : 1920;
|
// Assumes fixed 16:9 aspect ratio
|
||||||
|
return ([self getChosenStreamHeight] * 16) / 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void) saveSettings {
|
- (void) saveSettings {
|
||||||
@@ -120,7 +243,21 @@ static NSString* bitrateFormat = @"Bitrate: %.1f Mbps";
|
|||||||
NSInteger height = [self getChosenStreamHeight];
|
NSInteger height = [self getChosenStreamHeight];
|
||||||
NSInteger width = [self getChosenStreamWidth];
|
NSInteger width = [self getChosenStreamWidth];
|
||||||
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
NSInteger onscreenControls = [self.onscreenControlSelector selectedSegmentIndex];
|
||||||
[dataMan saveSettingsWithBitrate:_bitrate framerate:framerate height:height width:width onscreenControls:onscreenControls];
|
BOOL optimizeGames = [self.optimizeSettingsSelector selectedSegmentIndex] == 1;
|
||||||
|
BOOL multiController = [self.multiControllerSelector selectedSegmentIndex] == 1;
|
||||||
|
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
|
||||||
|
BOOL useHevc = [self.hevcSelector selectedSegmentIndex] == 1;
|
||||||
|
[dataMan saveSettingsWithBitrate:_bitrate
|
||||||
|
framerate:framerate
|
||||||
|
height:height
|
||||||
|
width:width
|
||||||
|
onscreenControls:onscreenControls
|
||||||
|
remote:NO
|
||||||
|
optimizeGames:optimizeGames
|
||||||
|
multiController:multiController
|
||||||
|
audioOnPC:audioOnPC
|
||||||
|
useHevc:useHevc
|
||||||
|
enableHdr:NO];
|
||||||
}
|
}
|
||||||
|
|
||||||
- (void)didReceiveMemoryWarning {
|
- (void)didReceiveMemoryWarning {
|
||||||
|
|||||||
Loaded 100 of 280 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user