new ui is almost fully functional

- add hosts
- pair to host
- get app list
- launch app
- resume app
This commit is contained in:
Diego Waxemberg
2014-10-26 02:15:53 -04:00
parent 6fbc55f193
commit 412c5c2516
37 changed files with 480 additions and 180 deletions

View File

@@ -47,6 +47,7 @@
FB8946EB19F6AFE100339C8A /* libcrypto.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E019F6AFB800339C8A /* libcrypto.a */; };
FB8946EC19F6AFE400339C8A /* libssl.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946E119F6AFB800339C8A /* libssl.a */; };
FB8946ED19F6AFE800339C8A /* libopus.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FB8946EA19F6AFB800339C8A /* libopus.a */; };
FBD3494319FC9C04002D2A60 /* AppManager.m in Sources */ = {isa = PBXBuildFile; fileRef = FBD3494219FC9C04002D2A60 /* AppManager.m */; };
FBDE86E019F7A837001C18A8 /* UIComputerView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86DF19F7A837001C18A8 /* UIComputerView.m */; };
FBDE86E619F82297001C18A8 /* UIAppView.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E519F82297001C18A8 /* UIAppView.m */; };
FBDE86E919F82315001C18A8 /* App.m in Sources */ = {isa = PBXBuildFile; fileRef = FBDE86E819F82315001C18A8 /* App.m */; };
@@ -214,12 +215,14 @@
FB8946E719F6AFB800339C8A /* opus_multistream.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_multistream.h; sourceTree = "<group>"; };
FB8946E819F6AFB800339C8A /* opus_types.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = opus_types.h; sourceTree = "<group>"; };
FB8946EA19F6AFB800339C8A /* libopus.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = libopus.a; sourceTree = "<group>"; };
FBD3494119FC9C04002D2A60 /* AppManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppManager.h; sourceTree = "<group>"; };
FBD3494219FC9C04002D2A60 /* AppManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppManager.m; sourceTree = "<group>"; };
FBDE86DE19F7A837001C18A8 /* UIComputerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIComputerView.h; sourceTree = "<group>"; };
FBDE86DF19F7A837001C18A8 /* UIComputerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIComputerView.m; sourceTree = "<group>"; };
FBDE86E419F82297001C18A8 /* UIAppView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = UIAppView.h; sourceTree = "<group>"; };
FBDE86E519F82297001C18A8 /* UIAppView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = UIAppView.m; sourceTree = "<group>"; };
FBDE86E719F82315001C18A8 /* App.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = App.h; sourceTree = "<group>"; };
FBDE86E819F82315001C18A8 /* App.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = App.m; sourceTree = "<group>"; };
FBDE86E719F82315001C18A8 /* App.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = App.h; path = ../App.h; sourceTree = "<group>"; };
FBDE86E819F82315001C18A8 /* App.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = App.m; path = ../App.m; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -320,8 +323,6 @@
FBDE86DF19F7A837001C18A8 /* UIComputerView.m */,
FBDE86E419F82297001C18A8 /* UIAppView.h */,
FBDE86E519F82297001C18A8 /* UIAppView.m */,
FBDE86E719F82315001C18A8 /* App.h */,
FBDE86E819F82315001C18A8 /* App.m */,
);
path = Limelight;
sourceTree = "<group>";
@@ -386,6 +387,8 @@
FB89461219F646E200339C8A /* MDNSManager.m */,
FB89461319F646E200339C8A /* PairManager.h */,
FB89461419F646E200339C8A /* PairManager.m */,
FBD3494119FC9C04002D2A60 /* AppManager.h */,
FBD3494219FC9C04002D2A60 /* AppManager.m */,
);
path = Network;
sourceTree = "<group>";
@@ -410,6 +413,8 @@
children = (
FB89461F19F646E200339C8A /* Computer.h */,
FB89462019F646E200339C8A /* Computer.m */,
FBDE86E719F82315001C18A8 /* App.h */,
FBDE86E819F82315001C18A8 /* App.m */,
FB89462119F646E200339C8A /* Utils.h */,
FB89462219F646E200339C8A /* Utils.m */,
);
@@ -725,6 +730,7 @@
FB89462819F646E200339C8A /* CryptoManager.m in Sources */,
FB89462E19F646E200339C8A /* PairManager.m in Sources */,
FB290D0019B2C406004C83CF /* main.m in Sources */,
FBD3494319FC9C04002D2A60 /* AppManager.m in Sources */,
FB89462A19F646E200339C8A /* ControllerSupport.m in Sources */,
FB89463119F646E200339C8A /* StreamManager.m in Sources */,
);

View File

@@ -10,6 +10,8 @@
@interface App : NSObject
@property NSString* displayName;
@property NSString* appId;
@property NSString* appName;
@property UIImage* appImage;
@end

View File

@@ -7,7 +7,9 @@
//
#import "App.h"
#import "HttpManager.h"
@implementation App
@synthesize appId, appName, appImage;
@end

View File

@@ -0,0 +1,23 @@
{
"images" : [
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "limelight_computer_add_1x.png"
},
{
"idiom" : "universal",
"scale" : "2x",
"filename" : "limelight_computer_add_2x.png"
},
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "limelight_computer_add_3x.png"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -3,7 +3,7 @@
{
"idiom" : "universal",
"scale" : "1x",
"filename" : "limelight_computer_2x-1.png"
"filename" : "limelight_computer_1x.png"
},
{
"idiom" : "universal",
@@ -13,7 +13,7 @@
{
"idiom" : "universal",
"scale" : "3x",
"filename" : "limelight_computer_2x-2.png"
"filename" : "limelight_computer_3x.png"
}
],
"info" : {

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -0,0 +1,23 @@
//
// AppManager.h
// Limelight
//
// Created by Diego Waxemberg on 10/25/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
//
#import <Foundation/Foundation.h>
#import "App.h"
#import "HttpManager.h"
@protocol AppAssetCallback <NSObject>
- (void) receivedAssetForApp:(App*)app;
@end
@interface AppManager : NSObject
+ (void) retrieveAppAssets:(NSArray*)apps withManager:(HttpManager*)hMan andCallback:(id<AppAssetCallback>)callback;
@end

View File

@@ -0,0 +1,45 @@
//
// AppManager.m
// Limelight
//
// Created by Diego Waxemberg on 10/25/14.
// Copyright (c) 2014 Limelight Stream. All rights reserved.
//
#import "AppManager.h"
@implementation AppManager {
App* _app;
HttpManager* _hMan;
id<AppAssetCallback> _callback;
}
+ (void) retrieveAppAssets:(NSArray*)apps withManager:(HttpManager*)hMan andCallback:(id<AppAssetCallback>)callback {
for (App* app in apps) {
AppManager* manager = [[AppManager alloc] initWithApp:app httpManager:hMan andCallback:callback];
[manager retrieveAsset];
}
}
- (id) initWithApp:(App*)app httpManager:(HttpManager*)hMan andCallback:(id<AppAssetCallback>)callback {
self = [super init];
_app = app;
_hMan = hMan;
_callback = callback;
return self;
}
- (void) retrieveAsset {
NSData* appAsset = [_hMan executeRequestSynchronously:[_hMan newAppAssetRequestWithAppId:_app.appId]];
UIImage* appImage = [UIImage imageWithData:appAsset];
_app.appImage = appImage;
NSLog(@"App Name: %@ id:%@ image: %@", _app.appName, _app.appId, _app.appImage);
[self performSelectorOnMainThread:@selector(sendCallBack) withObject:self waitUntilDone:NO];
}
- (void) sendCallBack {
[_callback receivedAssetForApp:_app];
}
@end

View File

@@ -10,6 +10,7 @@
@interface HttpManager : NSObject <NSURLConnectionDelegate, NSURLConnectionDataDelegate>
+ (NSArray*) getAppListFromXML:(NSData*)xml;
+ (NSString*) getStringFromXML:(NSData*)xml tag:(NSString*)tag;
+ (NSString*) getStatusStringFromXML:(NSData*)xml;
@@ -24,6 +25,7 @@
- (NSURLRequest*) newServerInfoRequest;
- (NSURLRequest*) newLaunchRequest:(NSString*)appId width:(int)width height:(int)height refreshRate:(int)refreshRate rikey:(NSString*)rikey rikeyid:(int)rikeyid;
- (NSURLRequest*) newResumeRequestWithRiKey:(NSString*)riKey riKeyId:(int)riKeyId;
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString*)appId;
- (NSData*) executeRequestSynchronously:(NSURLRequest*)request;
@end

View File

@@ -8,6 +8,7 @@
#import "HttpManager.h"
#import "CryptoManager.h"
#import "App.h"
#include <libxml2/libxml/xmlreader.h>
#include <string.h>
@@ -25,6 +26,60 @@
static const NSString* PORT = @"47984";
+ (NSArray*) getAppListFromXML:(NSData*)xml {
xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]);
if (docPtr == NULL) {
NSLog(@"ERROR: An error occured trying to parse xml.");
return NULL;
}
xmlNodePtr node;
xmlNodePtr rootNode = node = xmlDocGetRootElement(docPtr);
// Check root status_code
if (![HttpManager verifyStatus: rootNode]) {
NSLog(@"ERROR: Request returned with failure status");
return NULL;
}
// Skip the root node
node = node->children;
NSMutableArray* appList = [[NSMutableArray alloc] init];
while (node != NULL) {
NSLog(@"node: %s", node->name);
if (!xmlStrcmp(node->name, (const xmlChar*)"App")) {
xmlNodePtr appInfoNode = node->xmlChildrenNode;
NSString* appName;
NSString* appId;
while (appInfoNode != NULL) {
NSLog(@"appInfoNode: %s", appInfoNode->name);
if (!xmlStrcmp(appInfoNode->name, (const xmlChar*)"AppTitle")) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
appName = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
} else if (!xmlStrcmp(appInfoNode->name, (const xmlChar*)"ID")) {
xmlChar* nodeVal = xmlNodeListGetString(docPtr, appInfoNode->xmlChildrenNode, 1);
appId = [[NSString alloc] initWithCString:(const char*)nodeVal encoding:NSUTF8StringEncoding];
xmlFree(nodeVal);
}
appInfoNode = appInfoNode->next;
}
App* app = [[App alloc] init];
app.appName = appName;
app.appId = appId;
[appList addObject:app];
}
node = node->next;
}
xmlFree(rootNode);
xmlFree(docPtr);
return appList;
}
+ (NSString*) getStatusStringFromXML:(NSData*)xml {
xmlDocPtr docPtr = xmlParseMemory([xml bytes], (int)[xml length]);
@@ -198,6 +253,11 @@ static const NSString* PORT = @"47984";
return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSURLRequest*) newAppAssetRequestWithAppId:(NSString *)appId {
NSString* urlString = [NSString stringWithFormat:@"%@/appasset?uniqueid=%@&appid=%@&AssetType=2&AssetIdx=0", _baseURL, _uniqueId, appId];
return [self createRequestFromString:urlString enableTimeout:FALSE];
}
- (NSString*) bytesToHex:(NSData*)data {
const unsigned char* bytes = [data bytes];
NSMutableString *hex = [[NSMutableString alloc] init];
@@ -217,7 +277,11 @@ static const NSString* PORT = @"47984";
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection {
_requestResp = [HttpManager fixXmlVersion:_respData];
if ([[NSString alloc] initWithData:_respData encoding:NSUTF8StringEncoding] != nil) {
_requestResp = [HttpManager fixXmlVersion:_respData];
} else {
_requestResp = _respData;
}
dispatch_semaphore_signal(_requestLock);
}

View File

@@ -14,6 +14,7 @@
- (void) showPIN:(NSString*)PIN;
- (void) pairSuccessful;
- (void) pairFailed:(NSString*)message;
- (void) alreadyPaired;
@end

View File

@@ -32,14 +32,13 @@
[_callback pairFailed:@"Unable to connect to PC"];
return;
}
if (![[HttpManager getStringFromXML:serverInfo tag:@"currentgame"] isEqual:@"0"]) {
[_callback pairFailed:@"You must stop streaming before attempting to pair."];
}
else if (![[HttpManager getStringFromXML:serverInfo tag:@"PairStatus"] isEqual:@"1"]) {
[self initiatePair];
} else {
[_callback pairFailed:@"This device is already paired."];
[_callback alreadyPaired];
}
}

View File

@@ -11,6 +11,7 @@
@interface StreamConfiguration : NSObject
@property NSString* host;
@property NSString* appID;
@property int hostAddr;
@property int width;
@property int height;

View File

@@ -9,5 +9,5 @@
#import "StreamConfiguration.h"
@implementation StreamConfiguration
@synthesize host, hostAddr, width, height, frameRate, bitRate, riKeyId, riKey;
@synthesize host, appID, hostAddr, width, height, frameRate, bitRate, riKeyId, riKey;
@end

View File

@@ -42,6 +42,7 @@
NSData* serverInfoResp = [hMan executeRequestSynchronously:[hMan newServerInfoRequest]];
NSString* currentGame = [HttpManager getStringFromXML:serverInfoResp tag:@"currentgame"];
NSString* pairStatus = [HttpManager getStringFromXML:serverInfoResp tag:@"PairStatus"];
NSString* currentClient = [HttpManager getStringFromXML:serverInfoResp tag:@"CurrentClient"];
if (currentGame == NULL || pairStatus == NULL) {
[_callbacks launchFailed:@"Failed to connect to PC"];
return;
@@ -55,6 +56,11 @@
// resumeApp and launchApp handle calling launchFailed
if (![currentGame isEqualToString:@"0"]) {
if (![currentClient isEqualToString:@"1"]) {
// The server is streaming to someone else
[_callbacks launchFailed:@"There is another stream in progress"];
return;
}
// App already running, resume it
if (![self resumeApp:hMan]) {
return;
@@ -79,7 +85,7 @@
- (BOOL) launchApp:(HttpManager*)hMan {
NSData* launchResp = [hMan executeRequestSynchronously:
[hMan newLaunchRequest:@"67339056"
[hMan newLaunchRequest:_config.appID
width:_config.width
height:_config.height
refreshRate:_config.frameRate

View File

@@ -9,8 +9,15 @@
#import <UIKit/UIKit.h>
#import "App.h"
@interface UIAppView : UIView
@protocol AppCallback <NSObject>
- (id) initWithApp:(App*)app;
- (void) appClicked:(App*) app;
@end
@interface UIAppView : UIView
- (id) initWithApp:(App*)app andCallback:(id<AppCallback>)callback;
- (void) updateAppImage;
@end

View File

@@ -12,32 +12,54 @@
App* _app;
UIButton* _appButton;
UILabel* _appLabel;
id<AppCallback> _callback;
}
static int LABEL_DY = 20;
- (id) initWithApp:(App*)app {
- (id) initWithApp:(App*)app andCallback:(id<AppCallback>)callback {
self = [super init];
_app = app;
_callback = callback;
_appButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_appButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_appButton setBackgroundImage:[[UIImage imageNamed:@"Left4Dead2"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)] forState:UIControlStateNormal];
[_appButton setBackgroundImage:[UIImage imageNamed:@"NoAppImage"] forState:UIControlStateNormal];
[_appButton sizeToFit];
[_appButton addTarget:self action:@selector(appClicked) forControlEvents:UIControlEventTouchUpInside];
_appButton.layer.shadowColor = [[UIColor blackColor] CGColor];
_appButton.layer.shadowOffset = CGSizeMake(5,8);
_appButton.layer.shadowOpacity = 0.7;
_appLabel = [[UILabel alloc] init];
[_appLabel setText:_app.displayName];
[_appLabel setText:_app.appName];
[_appLabel sizeToFit];
_appLabel.center = CGPointMake(_appButton.bounds.origin.x + (_appButton.bounds.size.width / 2), _appButton.bounds.origin.y + _appButton.bounds.size.height + LABEL_DY);
[self updateBounds];
[self addSubview:_appButton];
[self addSubview:_appLabel];
self.frame = CGRectMake(0, 0, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height);
return self;
}
- (void) updateBounds {
float x = _appButton.frame.origin.x < _appLabel.frame.origin.x ? _appButton.frame.origin.x : _appLabel.frame.origin.x;
float y = _appButton.frame.origin.y < _appLabel.frame.origin.y ? _appButton.frame.origin.y : _appLabel.frame.origin.y;
self.bounds = CGRectMake(x , y, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height + LABEL_DY / 2);
self.frame = CGRectMake(x , y, _appButton.frame.size.width > _appLabel.frame.size.width ? _appButton.frame.size.width : _appLabel.frame.size.width, _appButton.frame.size.height + _appLabel.frame.size.height + LABEL_DY / 2);
}
- (void) appClicked {
[_callback appClicked:_app];
}
- (void) updateAppImage {
if (_app.appImage != nil) {
[_appButton setBackgroundImage:_app.appImage forState:UIControlStateNormal];
[self setNeedsDisplay];
}
}
/*
// Only override drawRect: if you perform custom drawing.

View File

@@ -9,8 +9,16 @@
#import <UIKit/UIKit.h>
#import "Computer.h"
@interface UIComputerView : UIView
@protocol HostCallback <NSObject>
- (id) initWithComputer:(Computer*)computer;
- (void) hostClicked:(Computer*)computer;
- (void) addHostClicked;
@end
@interface UIComputerView : UIView
- (id) initWithComputer:(Computer*)computer andCallback:(id<HostCallback>)callback;
- (id) initForAddWithCallback:(id<HostCallback>)callback;
@end

View File

@@ -12,31 +12,82 @@
Computer* _computer;
UIButton* _hostButton;
UILabel* _hostLabel;
id<HostCallback> _callback;
CGSize _labelSize;
}
static int LABEL_DY = 20;
- (id) initWithComputer:(Computer*)computer {
- (id) init {
self = [super init];
_computer = computer;
_hostButton = [UIButton buttonWithType:UIButtonTypeCustom];
[_hostButton setContentEdgeInsets:UIEdgeInsetsMake(0, 4, 0, 4)];
[_hostButton setBackgroundImage:[[UIImage imageNamed:@"Computer"] resizableImageWithCapInsets:UIEdgeInsetsMake(10, 10, 10, 10)] forState:UIControlStateNormal];
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
[_hostButton sizeToFit];
_hostButton.layer.shadowColor = [[UIColor blackColor] CGColor];
_hostButton.layer.shadowOffset = CGSizeMake(5,8);
_hostButton.layer.shadowOpacity = 0.7;
_hostLabel = [[UILabel alloc] init];
return self;
}
- (id) initWithComputer:(Computer*)computer andCallback:(id<HostCallback>)callback {
self = [self init];
_computer = computer;
_callback = callback;
[_hostLabel setText:[_computer displayName]];
[_hostLabel sizeToFit];
_hostLabel.center = CGPointMake(_hostButton.bounds.origin.x + (_hostButton.bounds.size.width / 2), _hostButton.bounds.origin.y + _hostButton.bounds.size.height + LABEL_DY);
[_hostButton addTarget:self action:@selector(hostClicked) forControlEvents:UIControlEventTouchUpInside];
_hostLabel.center = CGPointMake(_hostButton.frame.origin.x + (_hostButton.frame.size.width / 2), _hostButton.frame.origin.y + _hostButton.frame.size.height + LABEL_DY);
[self updateBounds];
[self addSubview:_hostButton];
[self addSubview:_hostLabel];
self.frame = CGRectMake(0, 0, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height);
return self;
}
- (void) updateBounds {
float x = _hostButton.frame.origin.x < _hostLabel.frame.origin.x ? _hostButton.frame.origin.x : _hostLabel.frame.origin.x;
float y = _hostButton.frame.origin.y < _hostLabel.frame.origin.y ? _hostButton.frame.origin.y : _hostLabel.frame.origin.y;
self.bounds = CGRectMake(x , y, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height + LABEL_DY / 2);
self.frame = CGRectMake(x , y, _hostButton.frame.size.width > _hostLabel.frame.size.width ? _hostButton.frame.size.width : _hostLabel.frame.size.width, _hostButton.frame.size.height + _hostLabel.frame.size.height + LABEL_DY / 2);
}
- (id) initForAddWithCallback:(id<HostCallback>)callback {
self = [self init];
_callback = callback;
[_hostButton setBackgroundImage:[UIImage imageNamed:@"Computer"] forState:UIControlStateNormal];
[_hostButton sizeToFit];
[_hostButton addTarget:self action:@selector(addClicked) forControlEvents:UIControlEventTouchUpInside];
[_hostLabel setText:@"Add Host"];
[_hostLabel sizeToFit];
_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"]];
[addIcon sizeToFit];
addIcon.center = CGPointMake(_hostButton.frame.origin.x + _hostButton.frame.size.width, _hostButton.frame.origin.y);
[self updateBounds];
[self addSubview:_hostButton];
[self addSubview:_hostLabel];
[self addSubview:addIcon];
return self;
}
- (void) hostClicked {
[_callback hostClicked:_computer];
}
- (void) addClicked {
[_callback addHostClicked];
}
/*
// Only override drawRect: if you perform custom drawing.
// An empty implementation adversely affects performance during animation.

View File

@@ -15,6 +15,5 @@
- (id) initWithHost:(NSNetService*)host;
- (id) initWithIp:(NSString*)host;
- (id) initPlaceholder;
@end

View File

@@ -28,13 +28,4 @@
return self;
}
- (id) initPlaceholder {
self = [super init];
self.hostName = NULL;
self.displayName = @"No computers found";
return self;
}
@end

View File

@@ -10,8 +10,11 @@
#import "MDNSManager.h"
#import "PairManager.h"
#import "StreamConfiguration.h"
#import "UIComputerView.h"
#import "UIAppView.h"
#import "AppManager.h"
@interface MainFrameViewController : UIViewController <MDNSCallback, PairCallback, NSURLConnectionDelegate>
@interface MainFrameViewController : UIViewController <MDNSCallback, PairCallback, HostCallback, AppCallback, AppAssetCallback, NSURLConnectionDelegate>
+ (StreamConfiguration*) getStreamConfiguration;

View File

@@ -1,4 +1,3 @@
// MainFrameViewController.m
// Limelight-iOS
//
@@ -22,45 +21,21 @@
NSOperationQueue* _opQueue;
MDNSManager* _mDNSManager;
Computer* _selectedHost;
NSString* _uniqueId;
NSData* _cert;
UIAlertView* _pairAlert;
UIScrollView* hostScrollView;
UIScrollView* appScrollView;
}
static NSString* deviceName = @"roth";
static NSMutableSet* hostList;
static StreamConfiguration* streamConfig;
+ (StreamConfiguration*) getStreamConfiguration {
return streamConfig;
}
//TODO: no more pair button
/*
- (void)PairButton:(UIButton *)sender
{
NSLog(@"Pair Button Pressed!");
if ([self.hostTextField.text length] > 0) {
_selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text];
NSLog(@"Using custom host: %@", self.hostTextField.text);
}
if (![self validatePcSelected]) {
NSLog(@"No valid PC selected");
return;
}
[CryptoManager generateKeyPairUsingSSl];
NSString* uniqueId = [CryptoManager getUniqueID];
NSData* cert = [CryptoManager readCertFromFile];
if ([Utils resolveHost:_selectedHost.hostName] == 0) {
[self displayDnsFailedDialog];
return;
}
HttpManager* hMan = [[HttpManager alloc] initWithHost:_selectedHost.hostName uniqueId:uniqueId deviceName:@"roth" cert:cert];
PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:cert callback:self];
[_opQueue addOperation:pMan];
}
*/
- (void)showPIN:(NSString *)PIN {
dispatch_sync(dispatch_get_main_queue(), ^{
_pairAlert = [[UIAlertView alloc] initWithTitle:@"Pairing" message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]delegate:self cancelButtonTitle:@"Ok" otherButtonTitles:nil, nil];
@@ -84,6 +59,26 @@ static StreamConfiguration* streamConfig;
});
}
- (void)alreadyPaired {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpManager* hMan = [[HttpManager alloc] initWithHost:_selectedHost.hostName uniqueId:_uniqueId deviceName:deviceName cert:_cert];
NSData* appListResp = [hMan executeRequestSynchronously:[hMan newAppListRequest]];
NSArray* appList = [HttpManager getAppListFromXML:appListResp];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateApps:appList];
});
[AppManager retrieveAppAssets:appList withManager:hMan andCallback:self];
});
}
- (void) receivedAssetForApp:(App*)app {
NSArray* subviews = [appScrollView subviews];
for (UIAppView* appView in subviews) {
[appView updateAppImage];
}
}
- (void)displayDnsFailedDialog {
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error"
message:@"Failed to resolve host."
@@ -92,31 +87,55 @@ static StreamConfiguration* streamConfig;
[self presentViewController:alert animated:YES completion:nil];
}
//TODO: No more stream button
/*
- (void)StreamButton:(UIButton *)sender
{
NSLog(@"Stream Button Pressed!");
if ([self.hostTextField.text length] > 0) {
_selectedHost = [[Computer alloc] initWithIp:self.hostTextField.text];
NSLog(@"Using custom host: %@", self.hostTextField.text);
}
if (![self validatePcSelected]) {
NSLog(@"No valid PC selected");
return;
}
- (void) hostClicked:(Computer *)computer {
NSLog(@"Clicked host: %@", computer.displayName);
_selectedHost = computer;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
HttpManager* hMan = [[HttpManager alloc] initWithHost:computer.hostName uniqueId:_uniqueId deviceName:deviceName cert:_cert];
NSData* serverInfoResp = [hMan executeRequestSynchronously:[hMan newServerInfoRequest]];
if ([[HttpManager getStringFromXML:serverInfoResp tag:@"PairStatus"] isEqualToString:@"1"]) {
NSLog(@"Already Paired");
[self alreadyPaired];
} else {
NSLog(@"Trying to pair");
PairManager* pMan = [[PairManager alloc] initWithManager:hMan andCert:_cert callback:self];
[_opQueue addOperation:pMan];
}
});
}
- (void) addHostClicked {
NSLog(@"Clicked add host");
UIAlertController* alertController = [UIAlertController alertControllerWithTitle:@"Host Address" message:@"Please enter a hostname or IP address" preferredStyle:UIAlertControllerStyleAlert];
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
[alertController addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
NSString* host = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
Computer* newHost = [[Computer alloc] initWithIp:host];
[hostList addObject:newHost];
[self updateHosts:[hostList allObjects]];
//TODO: get pair state
}]];
[alertController addTextFieldWithConfigurationHandler:nil];
[self presentViewController:alertController animated:YES completion:nil];
}
- (void) appClicked:(App *)app {
NSLog(@"Clicked app: %@", app.appName);
streamConfig = [[StreamConfiguration alloc] init];
streamConfig.host = _selectedHost.hostName;
streamConfig.hostAddr = [Utils resolveHost:_selectedHost.hostName];
streamConfig.appID = app.appId;
if (streamConfig.hostAddr == 0) {
[self displayDnsFailedDialog];
return;
}
unsigned long selectedConf = [self.StreamConfigs selectedRowInComponent:0];
// TODO: actually allow the user to choose the config
unsigned long selectedConf = 1;
NSLog(@"selectedConf: %ld", selectedConf);
switch (selectedConf) {
case 0:
@@ -148,19 +167,6 @@ static StreamConfiguration* streamConfig;
NSLog(@"StreamConfig: %@, %d, %dx%dx%d at %d Mbps", streamConfig.host, streamConfig.hostAddr, streamConfig.width, streamConfig.height, streamConfig.frameRate, streamConfig.bitRate);
[self performSegueWithIdentifier:@"createStreamFrame" sender:self];
}
*/
/*
- (void)setSelectedHost:(NSInteger)selectedIndex
{
_selectedHost = (Computer*)([self.hostPickerVals objectAtIndex:selectedIndex]);
if (_selectedHost.hostName == NULL) {
// This must be the placeholder computer
_selectedHost = NULL;
}
}
*/
- (void)viewDidLoad
{
@@ -169,68 +175,91 @@ static StreamConfiguration* streamConfig;
NSArray* streamConfigVals = [[NSArray alloc] initWithObjects:@"1280x720 (30Hz)", @"1280x720 (60Hz)", @"1920x1080 (30Hz)", @"1920x1080 (60Hz)",nil];
_opQueue = [[NSOperationQueue alloc] init];
[CryptoManager generateKeyPairUsingSSl];
_uniqueId = [CryptoManager getUniqueID];
_cert = [CryptoManager readCertFromFile];
// Initialize the host picker list
//[self updateHosts:[[NSArray alloc] init]];
if (hostList == nil) {
hostList = [[NSMutableSet alloc] init];
}
Computer* test = [[Computer alloc] initWithIp:@"CEMENT-TRUCK"];
UIScrollView* hostScrollView = [[UIScrollView alloc] init];
hostScrollView = [[UIScrollView alloc] init];
hostScrollView.frame = CGRectMake(0, 0, self.view.frame.size.width, self.view.frame.size.height / 2);
[hostScrollView setShowsHorizontalScrollIndicator:NO];
UIComputerView* compView;
for (int i = 0; i < 5; i++) {
compView = [[UIComputerView alloc] initWithComputer:test];
[hostScrollView addSubview:compView];
[compView sizeToFit];
compView.center = CGPointMake((compView.frame.size.width + 20) * i + compView.frame.size.width, hostScrollView.frame.size.height / 2);
}
[hostScrollView setContentSize:CGSizeMake(compView.frame.size.width * 5 + compView.frame.size.width, hostScrollView.frame.size.height)];
UIScrollView* appScrollView = [[UIScrollView alloc] init];
appScrollView = [[UIScrollView alloc] init];
appScrollView.frame = CGRectMake(0, hostScrollView.frame.size.height, self.view.frame.size.width, self.view.frame.size.height / 2);
[appScrollView setShowsHorizontalScrollIndicator:NO];
App* testApp = [[App alloc] init];
testApp.displayName = @"Left 4 Dead 2";
UIAppView* appView;
for (int i = 0; i < 5; i++) {
appView = [[UIAppView alloc] initWithApp:testApp];
[appScrollView addSubview:appView];
[appView sizeToFit];
appView.center = CGPointMake((appView.frame.size.width + 20) * i + compView.frame.size.width, appScrollView.frame.size.height / 2);
}
[appScrollView setContentSize:CGSizeMake(appView.frame.size.width * 5 + appView.frame.size.width, appScrollView.frame.size.height)];
[self updateHosts:[hostList allObjects]];
[self.view addSubview:hostScrollView];
[self.view addSubview:appScrollView];
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidDisappear:animated];
//_mDNSManager = [[MDNSManager alloc] initWithCallback:self];
// [_mDNSManager searchForHosts];
_mDNSManager = [[MDNSManager alloc] initWithCallback:self];
[_mDNSManager searchForHosts];
}
- (void)viewDidDisappear:(BOOL)animated
{
[super viewDidDisappear:animated];
// [_mDNSManager stopSearching];
[_mDNSManager stopSearching];
}
- (void)updateHosts:(NSArray *)hosts {
NSMutableArray *hostPickerValues = [[NSMutableArray alloc] initWithArray:hosts];
[hostList addObjectsFromArray:hosts];
[[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
UIComputerView* addComp = [[UIComputerView alloc] initForAddWithCallback:self];
UIComputerView* compView;
float prevEdge = -1;
for (Computer* comp in hostList) {
compView = [[UIComputerView alloc] initWithComputer:comp andCallback:self];
compView.center = CGPointMake([self getCompViewX:compView addComp:addComp prevEdge:prevEdge], hostScrollView.frame.size.height / 2);
prevEdge = compView.frame.origin.x + compView.frame.size.width;
[hostScrollView addSubview:compView];
}
if ([hostPickerValues count] == 0) {
[hostPickerValues addObject:[[Computer alloc] initPlaceholder]];
prevEdge = [self getCompViewX:addComp addComp:addComp prevEdge:prevEdge];
addComp.center = CGPointMake(prevEdge, hostScrollView.frame.size.height / 2);
[hostScrollView addSubview:addComp];
[hostScrollView setContentSize:CGSizeMake(prevEdge + addComp.frame.size.width, hostScrollView.frame.size.height)];
}
- (float) getCompViewX:(UIComputerView*)comp addComp:(UIComputerView*)addComp prevEdge:(float)prevEdge {
if (prevEdge == -1) {
return hostScrollView.frame.origin.x + comp.frame.size.width / 2 + addComp.frame.size.width / 2;
} else {
return prevEdge + addComp.frame.size.width / 2 + comp.frame.size.width / 2;
}
}
- (void) updateApps:(NSArray*)apps {
[[appScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
App* fakeApp = [[App alloc] init];
fakeApp.appName = @"No App Name";
UIAppView* noAppImage = [[UIAppView alloc] initWithApp:fakeApp andCallback:nil];
float prevEdge = -1;
UIAppView* appView;
for (App* app in apps) {
appView = [[UIAppView alloc] initWithApp:app andCallback:self];
prevEdge = [self getAppViewX:appView noApp:noAppImage prevEdge:prevEdge];
appView.center = CGPointMake(prevEdge, appScrollView.frame.size.height / 2);
prevEdge = appView.frame.origin.x + appView.frame.size.width;
[appScrollView addSubview:appView];
}
[appScrollView setContentSize:CGSizeMake(prevEdge + noAppImage.frame.size.width, appScrollView.frame.size.height)];
}
- (float) getAppViewX:(UIAppView*)app noApp:(UIAppView*)noAppImage prevEdge:(float)prevEdge {
if (prevEdge == -1) {
return appScrollView.frame.origin.x + app.frame.size.width / 2 + noAppImage.frame.size.width / 2;
} else {
return prevEdge + app.frame.size.width / 2 + noAppImage.frame.size.width / 2;
}
}

View File

@@ -26,6 +26,8 @@
[super viewDidLoad];
[self.stageLabel setText:@"Starting App"];
[self.stageLabel sizeToFit];
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
[UIApplication sharedApplication].idleTimerDisabled = YES;
@@ -53,6 +55,8 @@
dispatch_async(dispatch_get_main_queue(), ^{
[self.spinner stopAnimating];
[self.stageLabel setText:@"Waiting for first frame..."];
[self.stageLabel sizeToFit];
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
});
}
@@ -74,6 +78,8 @@
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
[self.stageLabel setText:titleCase];
[self.stageLabel sizeToFit];
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
});
}

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" useAutolayout="YES" initialViewController="wb7-af-jn8">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch.iPad" propertyAccessControl="none" initialViewController="wb7-af-jn8">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
</dependencies>
@@ -8,21 +8,9 @@
<scene sceneID="Me4-Nr-liz">
<objects>
<viewController id="wb7-af-jn8" customClass="MainFrameViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="bBC-Wp-X5U"/>
<viewControllerLayoutGuide type="bottom" id="QeP-va-uIv"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Icf-Kt-Ai7">
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="xyV-op-tQY">
<rect key="frame" x="564" y="470" width="42" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" cocoaTouchSystemColor="darkTextColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="1" alpha="1" colorSpace="calibratedWhite"/>
</view>
<simulatedOrientationMetrics key="simulatedOrientationMetrics" orientation="landscapeRight"/>
@@ -38,31 +26,23 @@
<scene sceneID="RuD-qk-7nb">
<objects>
<viewController storyboardIdentifier="MainFrame" id="OIm-0n-i9v" customClass="StreamFrameViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="NG3-N1-D4k"/>
<viewControllerLayoutGuide type="bottom" id="3MH-n6-BSR"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="VPm-Ae-rc4" userLabel="RenderView" customClass="StreamView">
<rect key="frame" x="0.0" y="0.0" width="1024" height="768"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="iOs-1X-mSU">
<activityIndicatorView opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" id="iOs-1X-mSU">
<rect key="frame" x="502" y="374" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="dDs-kT-po6">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="dDs-kT-po6">
<rect key="frame" x="491" y="402" width="42" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstItem="dDs-kT-po6" firstAttribute="top" secondItem="iOs-1X-mSU" secondAttribute="bottom" constant="8" id="VZj-wk-dHp"/>
<constraint firstAttribute="centerY" secondItem="iOs-1X-mSU" secondAttribute="centerY" id="YAN-0k-Rds"/>
<constraint firstAttribute="centerX" secondItem="dDs-kT-po6" secondAttribute="centerX" id="bLr-5Z-OH3"/>
<constraint firstAttribute="centerX" secondItem="iOs-1X-mSU" secondAttribute="centerX" id="ruD-8B-gj3"/>
</constraints>
</view>
<nil key="simulatedStatusBarMetrics"/>
<nil key="simulatedTopBarMetrics"/>

View File

@@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" initialViewController="dgh-JZ-Q7z">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="6250" systemVersion="14A388a" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" initialViewController="dgh-JZ-Q7z">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="6244"/>
</dependencies>
@@ -8,10 +8,6 @@
<scene sceneID="emz-kO-L7q">
<objects>
<viewController id="dgh-JZ-Q7z" customClass="MainFrameViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="udy-Vd-ca2"/>
<viewControllerLayoutGuide type="bottom" id="wiU-k0-6Q2"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="RId-Pb-IBX">
<rect key="frame" x="0.0" y="0.0" width="568" height="320"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
@@ -32,31 +28,23 @@
<scene sceneID="5Eb-q2-vjt">
<objects>
<viewController id="mI3-9F-XwU" customClass="StreamFrameViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="DRq-YB-9Rh"/>
<viewControllerLayoutGuide type="bottom" id="KH1-hM-RYW"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="eir-e9-IPE" customClass="StreamView">
<rect key="frame" x="0.0" y="0.0" width="568" height="320"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<subviews>
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" translatesAutoresizingMaskIntoConstraints="NO" id="0vm-Iv-K4b">
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" hidesWhenStopped="YES" animating="YES" style="white" id="0vm-Iv-K4b">
<rect key="frame" x="274" y="150" width="20" height="20"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
</activityIndicatorView>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Stage" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="2HK-Z5-4Ch">
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Stage" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" id="2HK-Z5-4Ch">
<rect key="frame" x="262" y="178" width="45" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="calibratedRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="calibratedWhite"/>
<constraints>
<constraint firstAttribute="centerX" secondItem="2HK-Z5-4Ch" secondAttribute="centerX" constant="-0.5" id="Blw-Hy-66z"/>
<constraint firstAttribute="centerX" secondItem="0vm-Iv-K4b" secondAttribute="centerX" id="mCQ-CP-Yik"/>
<constraint firstAttribute="centerY" secondItem="0vm-Iv-K4b" secondAttribute="centerY" id="t8e-qp-g1j"/>
<constraint firstItem="2HK-Z5-4Ch" firstAttribute="top" secondItem="0vm-Iv-K4b" secondAttribute="bottom" constant="8" id="tta-Uo-bBO"/>
</constraints>
</view>
<nil key="simulatedStatusBarMetrics"/>
<nil key="simulatedTopBarMetrics"/>