mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2025-07-01 23:35:59 +00:00
1197 lines
50 KiB
Objective-C
1197 lines
50 KiB
Objective-C
// MainFrameViewController.m
|
|
// Moonlight
|
|
//
|
|
// Created by Diego Waxemberg on 1/17/14.
|
|
// Copyright (c) 2014 Moonlight Stream. All rights reserved.
|
|
//
|
|
|
|
@import ImageIO;
|
|
|
|
#import "MainFrameViewController.h"
|
|
#import "CryptoManager.h"
|
|
#import "HttpManager.h"
|
|
#import "Connection.h"
|
|
#import "StreamManager.h"
|
|
#import "Utils.h"
|
|
#import "UIComputerView.h"
|
|
#import "UIAppView.h"
|
|
#import "DataManager.h"
|
|
#import "TemporarySettings.h"
|
|
#import "WakeOnLanManager.h"
|
|
#import "AppListResponse.h"
|
|
#import "ServerInfoResponse.h"
|
|
#import "StreamFrameViewController.h"
|
|
#import "LoadingFrameViewController.h"
|
|
#import "ComputerScrollView.h"
|
|
#import "TemporaryApp.h"
|
|
#import "IdManager.h"
|
|
#import "ConnectionHelper.h"
|
|
|
|
#if !TARGET_OS_TV
|
|
#import "SettingsViewController.h"
|
|
#else
|
|
#import <sys/utsname.h>
|
|
#endif
|
|
|
|
#import <VideoToolbox/VideoToolbox.h>
|
|
|
|
@implementation MainFrameViewController {
|
|
NSOperationQueue* _opQueue;
|
|
TemporaryHost* _selectedHost;
|
|
NSString* _uniqueId;
|
|
NSData* _clientCert;
|
|
DiscoveryManager* _discMan;
|
|
AppAssetManager* _appManager;
|
|
StreamConfiguration* _streamConfig;
|
|
UIAlertController* _pairAlert;
|
|
LoadingFrameViewController* _loadingFrame;
|
|
UIScrollView* hostScrollView;
|
|
FrontViewPosition currentPosition;
|
|
NSArray* _sortedAppList;
|
|
NSCache* _boxArtCache;
|
|
bool _background;
|
|
#if TARGET_OS_TV
|
|
UITapGestureRecognizer* _menuRecognizer;
|
|
#endif
|
|
}
|
|
static NSMutableSet* hostList;
|
|
|
|
- (void)showPIN:(NSString *)PIN {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
self->_pairAlert = [UIAlertController alertControllerWithTitle:@"Pairing"
|
|
message:[NSString stringWithFormat:@"Enter the following PIN on the host machine: %@", PIN]
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[self->_pairAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
|
|
self->_pairAlert = nil;
|
|
[self->_discMan startDiscovery];
|
|
[self hideLoadingFrame: ^{
|
|
[self showHostSelectionView];
|
|
}];
|
|
}]];
|
|
[[self activeViewController] presentViewController:self->_pairAlert animated:YES completion:nil];
|
|
});
|
|
}
|
|
|
|
- (void)displayPairingFailureDialog:(NSString *)message {
|
|
UIAlertController* failedDialog = [UIAlertController alertControllerWithTitle:@"Pairing Failed"
|
|
message:message
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[Utils addHelpOptionToDialog:failedDialog];
|
|
[failedDialog addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
|
|
[_discMan startDiscovery];
|
|
|
|
[self hideLoadingFrame: ^{
|
|
[self showHostSelectionView];
|
|
[[self activeViewController] presentViewController:failedDialog animated:YES completion:nil];
|
|
}];
|
|
}
|
|
|
|
- (void)pairFailed:(NSString *)message {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (self->_pairAlert != nil) {
|
|
[self->_pairAlert dismissViewControllerAnimated:YES completion:^{
|
|
[self displayPairingFailureDialog:message];
|
|
}];
|
|
self->_pairAlert = nil;
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void)pairSuccessful:(NSData*)serverCert {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Store the cert from pairing with the host
|
|
self->_selectedHost.serverCert = serverCert;
|
|
|
|
[self->_pairAlert dismissViewControllerAnimated:YES completion:nil];
|
|
self->_pairAlert = nil;
|
|
|
|
[self->_discMan startDiscovery];
|
|
[self alreadyPaired];
|
|
});
|
|
}
|
|
|
|
- (void)disableUpButton {
|
|
#if !TARGET_OS_TV
|
|
[self->_upButton setTitle:nil];
|
|
#endif
|
|
}
|
|
|
|
- (void)enableUpButton {
|
|
#if !TARGET_OS_TV
|
|
[self->_upButton setTitle:@"Select New Host"];
|
|
#endif
|
|
}
|
|
|
|
- (void)alreadyPaired {
|
|
BOOL usingCachedAppList = false;
|
|
|
|
// Capture the host here because it can change once we
|
|
// leave the main thread
|
|
TemporaryHost* host = _selectedHost;
|
|
|
|
if ([host.appList count] > 0) {
|
|
usingCachedAppList = true;
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (host != self->_selectedHost) {
|
|
[self hideLoadingFrame: nil];
|
|
return;
|
|
}
|
|
|
|
self.title = host.name;
|
|
|
|
[self enableUpButton];
|
|
[self updateAppsForHost:host];
|
|
[self hideLoadingFrame: nil];
|
|
});
|
|
}
|
|
Log(LOG_I, @"Using cached app list: %d", usingCachedAppList);
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
// Exempt this host from discovery while handling the applist query
|
|
[self->_discMan removeHostFromDiscovery:host];
|
|
|
|
AppListResponse* appListResp = [ConnectionHelper getAppListForHostWithHostIP:host.activeAddress serverCert:host.serverCert uniqueID:self->_uniqueId];
|
|
|
|
[self->_discMan addHostToDiscovery:host];
|
|
|
|
if (![appListResp isStatusOk] || [appListResp getAppList] == nil) {
|
|
Log(LOG_W, @"Failed to get applist: %@", appListResp.statusMessage);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (host != self->_selectedHost) {
|
|
[self hideLoadingFrame: nil];
|
|
return;
|
|
}
|
|
|
|
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Interrupted"
|
|
message:appListResp.statusMessage
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[Utils addHelpOptionToDialog:applistAlert];
|
|
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
[self hideLoadingFrame: ^{
|
|
[self showHostSelectionView];
|
|
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
|
|
}];
|
|
host.state = StateOffline;
|
|
});
|
|
} else {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self updateApplist:[appListResp getAppList] forHost:host];
|
|
|
|
if (host != self->_selectedHost) {
|
|
[self hideLoadingFrame: nil];
|
|
return;
|
|
}
|
|
|
|
self.title = host.name;
|
|
[self enableUpButton];
|
|
|
|
[self updateAppsForHost:host];
|
|
[self->_appManager stopRetrieving];
|
|
[self->_appManager retrieveAssetsFromHost:host];
|
|
[self hideLoadingFrame: nil];
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
- (void) updateApplist:(NSSet*) newList forHost:(TemporaryHost*)host {
|
|
DataManager* database = [[DataManager alloc] init];
|
|
|
|
for (TemporaryApp* app in newList) {
|
|
BOOL appAlreadyInList = NO;
|
|
for (TemporaryApp* savedApp in host.appList) {
|
|
if ([app.id isEqualToString:savedApp.id]) {
|
|
savedApp.name = app.name;
|
|
savedApp.hdrSupported = app.hdrSupported;
|
|
appAlreadyInList = YES;
|
|
break;
|
|
}
|
|
}
|
|
if (!appAlreadyInList) {
|
|
app.host = host;
|
|
[host.appList addObject:app];
|
|
}
|
|
}
|
|
|
|
BOOL appWasRemoved;
|
|
do {
|
|
appWasRemoved = NO;
|
|
|
|
for (TemporaryApp* app in host.appList) {
|
|
appWasRemoved = YES;
|
|
for (TemporaryApp* mergedApp in newList) {
|
|
if ([mergedApp.id isEqualToString:app.id]) {
|
|
appWasRemoved = NO;
|
|
break;
|
|
}
|
|
}
|
|
if (appWasRemoved) {
|
|
// Removing the app mutates the list we're iterating (which isn't legal).
|
|
// We need to jump out of this loop and restart enumeration.
|
|
|
|
[host.appList removeObject:app];
|
|
|
|
// It's important to remove the app record from the database
|
|
// since we'll have a constraint violation now that appList
|
|
// doesn't have this app in it.
|
|
[database removeApp:app];
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Keep looping until the list is no longer being mutated
|
|
} while (appWasRemoved);
|
|
|
|
[database updateAppsForExistingHost:host];
|
|
|
|
// This host may be eligible for a shortcut now that the app list
|
|
// has been populated
|
|
[self updateHostShortcuts];
|
|
}
|
|
|
|
- (void)showHostSelectionView {
|
|
#if TARGET_OS_TV
|
|
// Remove the menu button intercept to allow the app to exit
|
|
// when at the host selection view.
|
|
[self.navigationController.view removeGestureRecognizer:_menuRecognizer];
|
|
#endif
|
|
|
|
[_appManager stopRetrieving];
|
|
_selectedHost = nil;
|
|
_sortedAppList = nil;
|
|
|
|
self.title = @"Select Host";
|
|
[self disableUpButton];
|
|
|
|
[self.collectionView reloadData];
|
|
[self.view addSubview:hostScrollView];
|
|
}
|
|
|
|
- (void) receivedAssetForApp:(TemporaryApp*)app {
|
|
// Update the box art cache now so we don't have to do it
|
|
// on the main thread
|
|
[self updateBoxArtCacheForApp:app];
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self.collectionView reloadData];
|
|
});
|
|
}
|
|
|
|
- (void)displayDnsFailedDialog {
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Network Error"
|
|
message:@"Failed to resolve host."
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[Utils addHelpOptionToDialog:alert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
[[self activeViewController] presentViewController:alert animated:YES completion:nil];
|
|
}
|
|
|
|
- (void) hostClicked:(TemporaryHost *)host view:(UIView *)view {
|
|
// Treat clicks on offline hosts to be long clicks
|
|
// This shows the context menu with wake, delete, etc. rather
|
|
// than just hanging for a while and failing as we would in this
|
|
// code path.
|
|
if (host.state != StateOnline && view != nil) {
|
|
[self hostLongClicked:host view:view];
|
|
return;
|
|
}
|
|
|
|
Log(LOG_D, @"Clicked host: %@", host.name);
|
|
_selectedHost = host;
|
|
[self disableNavigation];
|
|
|
|
#if TARGET_OS_TV
|
|
// Intercept the menu key to go back to the host page
|
|
[self.navigationController.view addGestureRecognizer:_menuRecognizer];
|
|
#endif
|
|
|
|
// If we are online, paired, and have a cached app list, skip straight
|
|
// to the app grid without a loading frame. This is the fast path that users
|
|
// should hit most. Check for a valid view because we don't want to hit the fast
|
|
// path after coming back from streaming, since we need to fetch serverinfo too
|
|
// so that our active game data is correct.
|
|
if (host.state == StateOnline && host.pairState == PairStatePaired && host.appList.count > 0 && view != nil) {
|
|
[self alreadyPaired];
|
|
return;
|
|
}
|
|
|
|
[self showLoadingFrame: ^{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
HttpManager* hMan = [[HttpManager alloc] initWithHost:host.activeAddress uniqueId:self->_uniqueId serverCert:host.serverCert];
|
|
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
|
|
|
// Exempt this host from discovery while handling the serverinfo request
|
|
[self->_discMan removeHostFromDiscovery:host];
|
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
|
|
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
|
[self->_discMan addHostToDiscovery:host];
|
|
|
|
if (![serverInfoResp isStatusOk]) {
|
|
Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
if (host != self->_selectedHost) {
|
|
[self hideLoadingFrame:nil];
|
|
return;
|
|
}
|
|
|
|
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
|
message:serverInfoResp.statusMessage
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[Utils addHelpOptionToDialog:applistAlert];
|
|
[applistAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
|
|
// Only display an alert if this was the result of a real
|
|
// user action, not just passively entering the foreground again
|
|
[self hideLoadingFrame: ^{
|
|
[self showHostSelectionView];
|
|
if (view != nil) {
|
|
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
|
|
}
|
|
}];
|
|
|
|
host.state = StateOffline;
|
|
});
|
|
} else {
|
|
// Update the host object with this data
|
|
[serverInfoResp populateHost:host];
|
|
if (host.pairState == PairStatePaired) {
|
|
Log(LOG_I, @"Already Paired");
|
|
[self alreadyPaired];
|
|
}
|
|
// Only pair when this was the result of explicit user action
|
|
else if (view != nil) {
|
|
Log(LOG_I, @"Trying to pair");
|
|
// Polling the server while pairing causes the server to screw up
|
|
[self->_discMan stopDiscoveryBlocking];
|
|
PairManager* pMan = [[PairManager alloc] initWithManager:hMan clientCert:self->_clientCert callback:self];
|
|
[self->_opQueue addOperation:pMan];
|
|
}
|
|
else {
|
|
// Not user action, so just return to host screen
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self hideLoadingFrame:^{
|
|
[self showHostSelectionView];
|
|
}];
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}];
|
|
}
|
|
|
|
- (UIViewController*) activeViewController {
|
|
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
|
|
|
while (topController.presentedViewController) {
|
|
topController = topController.presentedViewController;
|
|
}
|
|
|
|
return topController;
|
|
}
|
|
|
|
- (void)hostLongClicked:(TemporaryHost *)host view:(UIView *)view {
|
|
Log(LOG_D, @"Long clicked host: %@", host.name);
|
|
NSString* message;
|
|
|
|
switch (host.state) {
|
|
case StateOffline:
|
|
message = @"Offline";
|
|
break;
|
|
|
|
case StateOnline:
|
|
if (host.pairState == PairStatePaired) {
|
|
message = @"Online - Paired";
|
|
}
|
|
else {
|
|
message = @"Online - Not Paired";
|
|
}
|
|
break;
|
|
|
|
case StateUnknown:
|
|
message = @"Connecting";
|
|
break;
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
|
|
if (host.state != StateOnline) {
|
|
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Wake" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
|
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:@"Wake-On-LAN" message:@"" preferredStyle:UIAlertControllerStyleAlert];
|
|
[wolAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
if (host.mac == nil || [host.mac isEqualToString:@"00:00:00:00:00:00"]) {
|
|
wolAlert.message = @"Host MAC unknown, unable to send WOL Packet";
|
|
} else {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
[WakeOnLanManager wakeHost:host];
|
|
});
|
|
wolAlert.message = @"Successfully sent wake-up request. It may take a few moments for the PC to wake. If it never wakes up, ensure it's properly configured for Wake-on-LAN.";
|
|
}
|
|
[[self activeViewController] presentViewController:wolAlert animated:YES completion:nil];
|
|
}]];
|
|
|
|
#if !TARGET_OS_TV
|
|
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Connection Help" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
|
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"https://github.com/moonlight-stream/moonlight-docs/wiki/Troubleshooting"]];
|
|
}]];
|
|
#endif
|
|
}
|
|
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Remove Host" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action) {
|
|
[self->_discMan removeHostFromDiscovery:host];
|
|
DataManager* dataMan = [[DataManager alloc] init];
|
|
[dataMan removeHost:host];
|
|
@synchronized(hostList) {
|
|
[hostList removeObject:host];
|
|
[self updateAllHosts:[hostList allObjects]];
|
|
}
|
|
|
|
}]];
|
|
[longClickAlert addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
|
|
|
// these two lines are required for iPad support of UIAlertSheet
|
|
longClickAlert.popoverPresentationController.sourceView = view;
|
|
|
|
longClickAlert.popoverPresentationController.sourceRect = CGRectMake(view.bounds.size.width / 2.0, view.bounds.size.height / 2.0, 1.0, 1.0); // center of the view
|
|
[[self activeViewController] presentViewController:longClickAlert animated:YES completion:^{
|
|
[self updateHosts];
|
|
}];
|
|
}
|
|
|
|
- (void) addHostClicked {
|
|
Log(LOG_D, @"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* hostAddress = ((UITextField*)[[alertController textFields] objectAtIndex:0]).text;
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
|
|
[self->_discMan discoverHost:hostAddress withCallback:^(TemporaryHost* host, NSString* error){
|
|
if (host != nil) {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
@synchronized(hostList) {
|
|
[hostList addObject:host];
|
|
}
|
|
[self updateHosts];
|
|
});
|
|
} else {
|
|
UIAlertController* hostNotFoundAlert = [UIAlertController alertControllerWithTitle:@"Add Host" message:error preferredStyle:UIAlertControllerStyleAlert];
|
|
[Utils addHelpOptionToDialog:hostNotFoundAlert];
|
|
[hostNotFoundAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[[self activeViewController] presentViewController:hostNotFoundAlert animated:YES completion:nil];
|
|
});
|
|
}
|
|
}];});
|
|
}]];
|
|
[alertController addTextFieldWithConfigurationHandler:nil];
|
|
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
|
|
}
|
|
|
|
- (void) prepareToStreamApp:(TemporaryApp *)app {
|
|
_streamConfig = [[StreamConfiguration alloc] init];
|
|
_streamConfig.host = app.host.activeAddress;
|
|
_streamConfig.appID = app.id;
|
|
_streamConfig.appName = app.name;
|
|
_streamConfig.serverCert = app.host.serverCert;
|
|
|
|
DataManager* dataMan = [[DataManager alloc] init];
|
|
TemporarySettings* streamSettings = [dataMan getSettings];
|
|
|
|
_streamConfig.frameRate = [streamSettings.framerate intValue];
|
|
if (@available(iOS 10.3, *)) {
|
|
// Don't stream more FPS than the display can show
|
|
if (_streamConfig.frameRate > [UIScreen mainScreen].maximumFramesPerSecond) {
|
|
_streamConfig.frameRate = (int)[UIScreen mainScreen].maximumFramesPerSecond;
|
|
Log(LOG_W, @"Clamping FPS to maximum refresh rate: %d", _streamConfig.frameRate);
|
|
}
|
|
}
|
|
|
|
_streamConfig.height = [streamSettings.height intValue];
|
|
_streamConfig.width = [streamSettings.width intValue];
|
|
#if TARGET_OS_TV
|
|
// Don't allow streaming 4K on the Apple TV HD
|
|
struct utsname systemInfo;
|
|
uname(&systemInfo);
|
|
if (strcmp(systemInfo.machine, "AppleTV5,3") == 0 && _streamConfig.height >= 2160) {
|
|
Log(LOG_W, @"4K streaming not supported on Apple TV HD");
|
|
_streamConfig.width = 1920;
|
|
_streamConfig.height = 1080;
|
|
}
|
|
#endif
|
|
|
|
_streamConfig.bitRate = [streamSettings.bitrate intValue];
|
|
_streamConfig.streamingRemotely = streamSettings.streamingRemotely;
|
|
_streamConfig.optimizeGameSettings = streamSettings.optimizeGames;
|
|
_streamConfig.playAudioOnPC = streamSettings.playAudioOnPC;
|
|
_streamConfig.allowHevc = streamSettings.useHevc;
|
|
|
|
// multiController must be set before calling getConnectedGamepadMask
|
|
_streamConfig.multiController = streamSettings.multiController;
|
|
_streamConfig.gamepadMask = [ControllerSupport getConnectedGamepadMask:_streamConfig];
|
|
|
|
|
|
// Probe for supported channel configurations
|
|
Log(LOG_I, @"Audio device supports %d channels", [AVAudioSession sharedInstance].maximumOutputNumberOfChannels);
|
|
if ([AVAudioSession sharedInstance].maximumOutputNumberOfChannels >= 6) {
|
|
_streamConfig.audioChannelCount = 6;
|
|
_streamConfig.audioChannelMask = 0xFC;
|
|
}
|
|
else {
|
|
_streamConfig.audioChannelCount = 2;
|
|
_streamConfig.audioChannelMask = 0x3;
|
|
}
|
|
|
|
// HDR requires HDR10 game, HDR10 display, and HEVC Main10 decoder on the client.
|
|
// It additionally requires an HEVC Main10 encoder on the server (GTX 1000+).
|
|
//
|
|
// It should also be a user preference, since some games may require higher peak
|
|
// brightness than the iOS device can support to look correct in HDR mode.
|
|
if (@available(iOS 11.3, *)) {
|
|
_streamConfig.enableHdr =
|
|
app.hdrSupported && // App supported
|
|
(app.host.serverCodecModeSupport & 0x200) != 0 && // HEVC Main10 encoding on host PC GPU
|
|
VTIsHardwareDecodeSupported(kCMVideoCodecType_HEVC) && // Decoder supported
|
|
(AVPlayer.availableHDRModes & AVPlayerHDRModeHDR10) != 0 && // Display supported
|
|
streamSettings.enableHdr; // User wants it enabled
|
|
}
|
|
}
|
|
|
|
- (void) appClicked:(TemporaryApp *)app {
|
|
Log(LOG_D, @"Clicked app: %@", app.name);
|
|
|
|
[_appManager stopRetrieving];
|
|
|
|
#if !TARGET_OS_TV
|
|
if (currentPosition != FrontViewPositionLeft) {
|
|
// This must not be animated because we need the position
|
|
// to change (and notify our callback to save settings data)
|
|
// before we call prepareToStreamApp.
|
|
[[self revealViewController] revealToggleAnimated:NO];
|
|
}
|
|
#endif
|
|
|
|
TemporaryApp* currentApp = [self findRunningApp:app.host];
|
|
if (currentApp != nil) {
|
|
UIAlertController* alertController = [UIAlertController
|
|
alertControllerWithTitle: app.name
|
|
message: [app.id isEqualToString:currentApp.id] ? @"" : [NSString stringWithFormat:@"%@ is currently running", currentApp.name]preferredStyle:UIAlertControllerStyleAlert];
|
|
[alertController addAction:[UIAlertAction
|
|
actionWithTitle:[app.id isEqualToString:currentApp.id] ? @"Resume App" : @"Resume Running App" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
|
Log(LOG_I, @"Resuming application: %@", currentApp.name);
|
|
[self prepareToStreamApp:currentApp];
|
|
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
|
}]];
|
|
[alertController addAction:[UIAlertAction actionWithTitle:
|
|
[app.id isEqualToString:currentApp.id] ? @"Quit App" : @"Quit Running App and Start" style:UIAlertActionStyleDestructive handler:^(UIAlertAction* action){
|
|
Log(LOG_I, @"Quitting application: %@", currentApp.name);
|
|
[self showLoadingFrame: ^{
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
|
|
HttpManager* hMan = [[HttpManager alloc] initWithHost:app.host.activeAddress uniqueId:self->_uniqueId serverCert:app.host.serverCert];
|
|
HttpResponse* quitResponse = [[HttpResponse alloc] init];
|
|
HttpRequest* quitRequest = [HttpRequest requestForResponse: quitResponse withUrlRequest:[hMan newQuitAppRequest]];
|
|
|
|
// Exempt this host from discovery while handling the quit operation
|
|
[self->_discMan removeHostFromDiscovery:app.host];
|
|
[hMan executeRequestSynchronously:quitRequest];
|
|
if (quitResponse.statusCode == 200) {
|
|
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
|
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
|
|
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
|
if (![serverInfoResp isStatusOk] || [[serverInfoResp getStringTag:@"state"] hasSuffix:@"_SERVER_BUSY"]) {
|
|
// On newer GFE versions, the quit request succeeds even though the app doesn't
|
|
// really quit if another client tries to kill your app. We'll patch the response
|
|
// to look like the old error in that case, so the UI behaves.
|
|
quitResponse.statusCode = 599;
|
|
}
|
|
else if ([serverInfoResp isStatusOk]) {
|
|
// Update the host object with this info
|
|
[serverInfoResp populateHost:app.host];
|
|
}
|
|
}
|
|
[self->_discMan addHostToDiscovery:app.host];
|
|
|
|
// If it fails, display an error and stop the current operation
|
|
if (quitResponse.statusCode != 200) {
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Quitting App Failed"
|
|
message:@"Failed to quit app. If this app was started by "
|
|
"another device, you'll need to quit from that device."
|
|
preferredStyle:UIAlertControllerStyleAlert];
|
|
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil]];
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self updateAppsForHost:app.host];
|
|
[self hideLoadingFrame: ^{
|
|
[[self activeViewController] presentViewController:alert animated:YES completion:nil];
|
|
}];
|
|
});
|
|
}
|
|
else {
|
|
app.host.currentGame = @"0";
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Refresh the UI
|
|
[self updateAppsForHost:app.host];
|
|
|
|
// If it succeeds and we're to start streaming, segue to the stream
|
|
if (![app.id isEqualToString:currentApp.id]) {
|
|
[self prepareToStreamApp:app];
|
|
[self hideLoadingFrame: ^{
|
|
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
|
}];
|
|
}
|
|
else {
|
|
// Otherwise, just hide the loading icon
|
|
[self hideLoadingFrame:nil];
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}];
|
|
|
|
}]];
|
|
[alertController addAction:[UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil]];
|
|
[[self activeViewController] presentViewController:alertController animated:YES completion:nil];
|
|
} else {
|
|
[self prepareToStreamApp:app];
|
|
[self performSegueWithIdentifier:@"createStreamFrame" sender:nil];
|
|
}
|
|
}
|
|
|
|
- (TemporaryApp*) findRunningApp:(TemporaryHost*)host {
|
|
for (TemporaryApp* app in host.appList) {
|
|
if ([app.id isEqualToString:host.currentGame]) {
|
|
return app;
|
|
}
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
#if !TARGET_OS_TV
|
|
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
|
|
// If we moved back to the center position, we should save the settings
|
|
if (position == FrontViewPositionLeft) {
|
|
[(SettingsViewController*)[revealController rearViewController] saveSettings];
|
|
}
|
|
|
|
currentPosition = position;
|
|
}
|
|
#endif
|
|
|
|
#if TARGET_OS_TV
|
|
- (void)collectionView:(UICollectionView *)collectionView didSelectItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
[self appClicked:_sortedAppList[indexPath.row]];
|
|
}
|
|
#endif
|
|
|
|
- (void)prepareForSegue:(UIStoryboardSegue *)segue sender:(id)sender {
|
|
if ([segue.destinationViewController isKindOfClass:[StreamFrameViewController class]]) {
|
|
StreamFrameViewController* streamFrame = segue.destinationViewController;
|
|
streamFrame.streamConfig = _streamConfig;
|
|
}
|
|
}
|
|
|
|
- (void) showLoadingFrame:(void (^)(void))completion {
|
|
[_loadingFrame showLoadingFrame:completion];
|
|
}
|
|
|
|
- (void) hideLoadingFrame:(void (^)(void))completion {
|
|
[self enableNavigation];
|
|
[_loadingFrame dismissLoadingFrame:completion];
|
|
}
|
|
|
|
- (void)adjustScrollViewForSafeArea:(UIScrollView*)view {
|
|
if (@available(iOS 11.0, *)) {
|
|
if (self.view.safeAreaInsets.left >= 20 || self.view.safeAreaInsets.right >= 20) {
|
|
view.contentInset = UIEdgeInsetsMake(0, 20, 0, 20);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust the subviews for the safe area on the iPhone X.
|
|
- (void)viewSafeAreaInsetsDidChange {
|
|
[super viewSafeAreaInsetsDidChange];
|
|
|
|
[self adjustScrollViewForSafeArea:self.collectionView];
|
|
[self adjustScrollViewForSafeArea:self->hostScrollView];
|
|
}
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
self.title = @"Select Host";
|
|
|
|
#if !TARGET_OS_TV
|
|
// Set the side bar button action. When it's tapped, it'll show the sidebar.
|
|
[_settingsButton setTarget:self.revealViewController];
|
|
[_settingsButton setAction:@selector(revealToggle:)];
|
|
|
|
// Set the host name button action. When it's tapped, it'll show the host selection view.
|
|
[_upButton setTarget:self];
|
|
[_upButton setAction:@selector(showHostSelectionView)];
|
|
[self disableUpButton];
|
|
|
|
// Set the gesture
|
|
[self.view addGestureRecognizer:self.revealViewController.panGestureRecognizer];
|
|
|
|
// Get callbacks associated with the viewController
|
|
[self.revealViewController setDelegate:self];
|
|
|
|
// Disable bounce-back on reveal VC otherwise the settings will snap closed
|
|
// if the user drags all the way off the screen opposite the settings pane.
|
|
self.revealViewController.bounceBackOnOverdraw = NO;
|
|
#else
|
|
// The settings button will direct the user into the Settings app on tvOS
|
|
[_settingsButton setTarget:self];
|
|
[_settingsButton setAction:@selector(openTvSettings:)];
|
|
|
|
// Restore focus on the selected app on view controller pop navigation
|
|
self.restoresFocusAfterTransition = NO;
|
|
self.collectionView.remembersLastFocusedIndexPath = YES;
|
|
|
|
_menuRecognizer = [[UITapGestureRecognizer alloc] init];
|
|
[_menuRecognizer addTarget:self action: @selector(showHostSelectionView)];
|
|
_menuRecognizer.allowedPressTypes = [[NSArray alloc] initWithObjects:[NSNumber numberWithLong:UIPressTypeMenu], nil];
|
|
|
|
self.navigationController.navigationBar.titleTextAttributes = [NSDictionary dictionaryWithObject:[UIColor whiteColor] forKey:NSForegroundColorAttributeName];
|
|
#endif
|
|
|
|
_loadingFrame = [self.storyboard instantiateViewControllerWithIdentifier:@"loadingFrame"];
|
|
|
|
// Set the current position to the center
|
|
currentPosition = FrontViewPositionLeft;
|
|
|
|
// Set up crypto
|
|
[CryptoManager generateKeyPairUsingSSL];
|
|
_uniqueId = [IdManager getUniqueId];
|
|
_clientCert = [CryptoManager readCertFromFile];
|
|
|
|
_appManager = [[AppAssetManager alloc] initWithCallback:self];
|
|
_opQueue = [[NSOperationQueue alloc] init];
|
|
|
|
// Only initialize the host picker list once
|
|
if (hostList == nil) {
|
|
hostList = [[NSMutableSet alloc] init];
|
|
}
|
|
|
|
_boxArtCache = [[NSCache alloc] init];
|
|
|
|
hostScrollView = [[ComputerScrollView alloc] init];
|
|
hostScrollView.frame = CGRectMake(0, self.navigationController.navigationBar.frame.origin.y + self.navigationController.navigationBar.frame.size.height, self.view.frame.size.width, self.view.frame.size.height / 2);
|
|
[hostScrollView setShowsHorizontalScrollIndicator:NO];
|
|
hostScrollView.delaysContentTouches = NO;
|
|
|
|
self.collectionView.delaysContentTouches = NO;
|
|
self.collectionView.allowsMultipleSelection = NO;
|
|
#if !TARGET_OS_TV
|
|
self.collectionView.multipleTouchEnabled = NO;
|
|
#endif
|
|
|
|
[self retrieveSavedHosts];
|
|
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
|
|
|
|
[self.view addSubview:hostScrollView];
|
|
}
|
|
|
|
#if TARGET_OS_TV
|
|
- (void)openTvSettings:(id)sender
|
|
{
|
|
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString] options:@{} completionHandler:nil];
|
|
}
|
|
#endif
|
|
|
|
-(void)beginForegroundRefresh:(bool)refreshAppList
|
|
{
|
|
if (!_background) {
|
|
// This will kick off box art caching
|
|
[self updateHosts];
|
|
|
|
[_discMan startDiscovery];
|
|
|
|
// This will refresh the applist when a paired host is selected
|
|
if (refreshAppList && _selectedHost != nil && _selectedHost.pairState == PairStatePaired) {
|
|
[self hostClicked:_selectedHost view:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void)handlePendingShortcutAction
|
|
{
|
|
// Check if we have a pending shortcut action
|
|
AppDelegate* delegate = (AppDelegate*)[UIApplication sharedApplication].delegate;
|
|
if (delegate.pcUuidToLoad != nil) {
|
|
// Find the host it corresponds to
|
|
TemporaryHost* matchingHost = nil;
|
|
for (TemporaryHost* host in hostList) {
|
|
if ([host.uuid isEqualToString:delegate.pcUuidToLoad]) {
|
|
matchingHost = host;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Clear the pending shortcut action
|
|
delegate.pcUuidToLoad = nil;
|
|
|
|
// Complete the request
|
|
if (delegate.shortcutCompletionHandler != nil) {
|
|
delegate.shortcutCompletionHandler(matchingHost != nil);
|
|
delegate.shortcutCompletionHandler = nil;
|
|
}
|
|
|
|
if (matchingHost != nil && _selectedHost != matchingHost) {
|
|
// Navigate to the host page
|
|
[self hostClicked:matchingHost view:nil];
|
|
}
|
|
}
|
|
}
|
|
|
|
-(void)handleReturnToForeground
|
|
{
|
|
_background = NO;
|
|
|
|
[self beginForegroundRefresh: YES];
|
|
|
|
// Check for a pending shortcut action when returning to foreground
|
|
[self handlePendingShortcutAction];
|
|
}
|
|
|
|
-(void)handleEnterBackground
|
|
{
|
|
_background = YES;
|
|
|
|
[_discMan stopDiscovery];
|
|
}
|
|
|
|
- (void)viewDidAppear:(BOOL)animated
|
|
{
|
|
[super viewDidAppear:animated];
|
|
|
|
#if !TARGET_OS_TV
|
|
[[self revealViewController] setPrimaryViewController:self];
|
|
#endif
|
|
|
|
[self.navigationController setNavigationBarHidden:NO animated:YES];
|
|
|
|
// Hide 1px border line
|
|
UIImage* fakeImage = [[UIImage alloc] init];
|
|
[self.navigationController.navigationBar setShadowImage:fakeImage];
|
|
[self.navigationController.navigationBar setBackgroundImage:fakeImage forBarPosition:UIBarPositionAny barMetrics:UIBarMetricsDefault];
|
|
|
|
// Check for a pending shortcut action when appearing
|
|
[self handlePendingShortcutAction];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver: self
|
|
selector: @selector(handleReturnToForeground)
|
|
name: UIApplicationDidBecomeActiveNotification
|
|
object: nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver: self
|
|
selector: @selector(handleEnterBackground)
|
|
name: UIApplicationWillResignActiveNotification
|
|
object: nil];
|
|
}
|
|
|
|
- (void)viewWillAppear:(BOOL)animated
|
|
{
|
|
[super viewWillAppear:animated];
|
|
|
|
// We can get here on home press while streaming
|
|
// since the stream view segues to us just before
|
|
// entering the background. We can't check the app
|
|
// state here (since it's in transition), so we have
|
|
// to use this function that will use our internal
|
|
// state here to determine whether we're foreground.
|
|
//
|
|
// Note that this is neccessary here as we may enter
|
|
// this view via an error dialog from the stream
|
|
// view, so we won't get a return to active notification
|
|
// for that which would normally fire beginForegroundRefresh.
|
|
//
|
|
// On tvOS, we'll get a viewWillAppear when returning from the
|
|
// loading frame which will cause an infinite loop by starting
|
|
// another loading frame. To avoid this, just don't refresh
|
|
// if we're coming back from a loading frame view.
|
|
[self beginForegroundRefresh: !([_loadingFrame isShown] || [_loadingFrame isBeingDismissed])];
|
|
}
|
|
|
|
- (void)viewDidDisappear:(BOOL)animated
|
|
{
|
|
[super viewDidDisappear:animated];
|
|
|
|
// when discovery stops, we must create a new instance because
|
|
// you cannot restart an NSOperation when it is finished
|
|
[_discMan stopDiscovery];
|
|
|
|
// Purge the box art cache
|
|
[_boxArtCache removeAllObjects];
|
|
|
|
// Remove our lifetime observers to avoid triggering them
|
|
// while streaming
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
}
|
|
|
|
- (void) retrieveSavedHosts {
|
|
DataManager* dataMan = [[DataManager alloc] init];
|
|
NSArray* hosts = [dataMan getHosts];
|
|
@synchronized(hostList) {
|
|
[hostList addObjectsFromArray:hosts];
|
|
|
|
// Initialize the non-persistent host state
|
|
for (TemporaryHost* host in hostList) {
|
|
if (host.activeAddress == nil) {
|
|
host.activeAddress = host.localAddress;
|
|
}
|
|
if (host.activeAddress == nil) {
|
|
host.activeAddress = host.externalAddress;
|
|
}
|
|
if (host.activeAddress == nil) {
|
|
host.activeAddress = host.address;
|
|
}
|
|
if (host.activeAddress == nil) {
|
|
host.activeAddress = host.ipv6Address;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) updateAllHosts:(NSArray *)hosts {
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
Log(LOG_D, @"New host list:");
|
|
for (TemporaryHost* host in hosts) {
|
|
Log(LOG_D, @"Host: \n{\n\t name:%@ \n\t address:%@ \n\t localAddress:%@ \n\t externalAddress:%@ \n\t ipv6Address:%@ \n\t uuid:%@ \n\t mac:%@ \n\t pairState:%d \n\t online:%d \n\t activeAddress:%@ \n}", host.name, host.address, host.localAddress, host.externalAddress, host.ipv6Address, host.uuid, host.mac, host.pairState, host.state, host.activeAddress);
|
|
}
|
|
@synchronized(hostList) {
|
|
[hostList removeAllObjects];
|
|
[hostList addObjectsFromArray:hosts];
|
|
}
|
|
[self updateHosts];
|
|
});
|
|
}
|
|
|
|
- (void)updateHostShortcuts {
|
|
if (@available(iOS 9.0, *)) {
|
|
NSMutableArray* quickActions = [[NSMutableArray alloc] init];
|
|
|
|
@synchronized (hostList) {
|
|
for (TemporaryHost* host in hostList) {
|
|
// Pair state may be unknown if we haven't polled it yet, but the app list
|
|
// count will persist from paired PCs
|
|
if ([host.appList count] > 0) {
|
|
UIApplicationShortcutItem* shortcut = [[UIApplicationShortcutItem alloc]
|
|
initWithType:@"PC"
|
|
localizedTitle:host.name
|
|
localizedSubtitle:nil
|
|
icon:[UIApplicationShortcutIcon iconWithType:UIApplicationShortcutIconTypePlay]
|
|
userInfo:[NSDictionary dictionaryWithObject:host.uuid forKey:@"UUID"]];
|
|
[quickActions addObject: shortcut];
|
|
}
|
|
}
|
|
}
|
|
|
|
[UIApplication sharedApplication].shortcutItems = quickActions;
|
|
}
|
|
}
|
|
|
|
- (void)updateHosts {
|
|
Log(LOG_I, @"Updating hosts...");
|
|
[[hostScrollView subviews] makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
|
UIComputerView* addComp = [[UIComputerView alloc] initForAddWithCallback:self];
|
|
UIComputerView* compView;
|
|
float prevEdge = -1;
|
|
@synchronized (hostList) {
|
|
// Sort the host list in alphabetical order
|
|
NSArray* sortedHostList = [[hostList allObjects] sortedArrayUsingSelector:@selector(compareName:)];
|
|
for (TemporaryHost* comp in sortedHostList) {
|
|
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];
|
|
|
|
// Start jobs to decode the box art in advance
|
|
for (TemporaryApp* app in comp.appList) {
|
|
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0), ^{
|
|
[self updateBoxArtCacheForApp:app];
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Create or delete host shortcuts as needed
|
|
[self updateHostShortcuts];
|
|
|
|
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 {
|
|
float padding;
|
|
|
|
#if TARGET_OS_TV
|
|
padding = 100;
|
|
#else
|
|
padding = addComp.frame.size.width / 2;
|
|
#endif
|
|
|
|
if (prevEdge == -1) {
|
|
return hostScrollView.frame.origin.x + comp.frame.size.width / 2 + padding;
|
|
} else {
|
|
return prevEdge + comp.frame.size.width / 2 + padding;
|
|
}
|
|
}
|
|
|
|
// This function forces immediate decoding of the UIImage, rather
|
|
// than the default lazy decoding that results in janky scrolling.
|
|
+ (UIImage*) loadBoxArtForCaching:(TemporaryApp*)app {
|
|
UIImage* boxArt;
|
|
|
|
NSData* imageData = [NSData dataWithContentsOfFile:[AppAssetManager boxArtPathForApp:app]];
|
|
if (imageData == nil) {
|
|
// No box art on disk
|
|
return nil;
|
|
}
|
|
|
|
CGImageSourceRef source = CGImageSourceCreateWithData((__bridge CFDataRef)imageData, NULL);
|
|
CGImageRef cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil);
|
|
|
|
size_t width = CGImageGetWidth(cgImage);
|
|
size_t height = CGImageGetHeight(cgImage);
|
|
|
|
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
|
CGContextRef imageContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace,
|
|
kCGImageAlphaPremultipliedFirst | kCGBitmapByteOrder32Little);
|
|
CGColorSpaceRelease(colorSpace);
|
|
|
|
CGContextDrawImage(imageContext, CGRectMake(0, 0, width, height), cgImage);
|
|
|
|
CGImageRef outputImage = CGBitmapContextCreateImage(imageContext);
|
|
|
|
boxArt = [UIImage imageWithCGImage:outputImage];
|
|
|
|
CGImageRelease(outputImage);
|
|
CGContextRelease(imageContext);
|
|
|
|
CGImageRelease(cgImage);
|
|
CFRelease(source);
|
|
|
|
return boxArt;
|
|
}
|
|
|
|
- (void) updateBoxArtCacheForApp:(TemporaryApp*)app {
|
|
if ([_boxArtCache objectForKey:app] == nil) {
|
|
UIImage* image = [MainFrameViewController loadBoxArtForCaching:app];
|
|
if (image != nil) {
|
|
// Add the image to our cache if it was present
|
|
[_boxArtCache setObject:image forKey:app];
|
|
}
|
|
}
|
|
}
|
|
|
|
- (void) updateAppsForHost:(TemporaryHost*)host {
|
|
if (host != _selectedHost) {
|
|
Log(LOG_W, @"Mismatched host during app update");
|
|
return;
|
|
}
|
|
|
|
_sortedAppList = [host.appList allObjects];
|
|
_sortedAppList = [_sortedAppList sortedArrayUsingSelector:@selector(compareName:)];
|
|
|
|
[hostScrollView removeFromSuperview];
|
|
[self.collectionView reloadData];
|
|
}
|
|
|
|
- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath {
|
|
UICollectionViewCell* cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"AppCell" forIndexPath:indexPath];
|
|
|
|
TemporaryApp* app = _sortedAppList[indexPath.row];
|
|
UIAppView* appView = [[UIAppView alloc] initWithApp:app cache:_boxArtCache andCallback:self];
|
|
|
|
if (appView.bounds.size.width > 10.0) {
|
|
CGFloat scale = cell.bounds.size.width / appView.bounds.size.width;
|
|
[appView setCenter:CGPointMake(appView.bounds.size.width / 2 * scale, appView.bounds.size.height / 2 * scale)];
|
|
appView.transform = CGAffineTransformMakeScale(scale, scale);
|
|
}
|
|
|
|
[cell.subviews.firstObject removeFromSuperview]; // Remove a view that was previously added
|
|
[cell addSubview:appView];
|
|
|
|
|
|
UIBezierPath *shadowPath = [UIBezierPath bezierPathWithRect:cell.bounds];
|
|
cell.layer.masksToBounds = NO;
|
|
cell.layer.shadowColor = [UIColor blackColor].CGColor;
|
|
cell.layer.shadowOffset = CGSizeMake(1.0f, 5.0f);
|
|
cell.layer.shadowOpacity = 0.5f;
|
|
cell.layer.shadowPath = shadowPath.CGPath;
|
|
|
|
#if !TARGET_OS_TV
|
|
cell.layer.borderWidth = 1;
|
|
cell.layer.borderColor = [[UIColor colorWithRed:0 green:0 blue:0 alpha:0.3f] CGColor];
|
|
cell.exclusiveTouch = YES;
|
|
#endif
|
|
|
|
return cell;
|
|
}
|
|
|
|
- (NSInteger)numberOfSectionsInCollectionView:(UICollectionView *)collectionView {
|
|
return 1; // App collection only
|
|
}
|
|
|
|
- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section {
|
|
if (_selectedHost != nil && _sortedAppList != nil) {
|
|
return _sortedAppList.count;
|
|
}
|
|
else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
- (void)didReceiveMemoryWarning
|
|
{
|
|
[super didReceiveMemoryWarning];
|
|
|
|
// Purge the box art cache on low memory
|
|
[_boxArtCache removeAllObjects];
|
|
}
|
|
|
|
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
|
|
[self.view endEditing:YES];
|
|
}
|
|
|
|
- (BOOL)textFieldShouldReturn:(UITextField *)textField {
|
|
[textField resignFirstResponder];
|
|
return YES;
|
|
}
|
|
|
|
#if !TARGET_OS_TV
|
|
- (BOOL)shouldAutorotate {
|
|
return YES;
|
|
}
|
|
#endif
|
|
|
|
- (void) disableNavigation {
|
|
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = NO;
|
|
}
|
|
|
|
- (void) enableNavigation {
|
|
self.navigationController.navigationBar.topItem.rightBarButtonItem.enabled = YES;
|
|
}
|
|
|
|
#if TARGET_OS_TV
|
|
- (BOOL)canBecomeFocused {
|
|
return YES;
|
|
}
|
|
#endif
|
|
|
|
- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator {
|
|
|
|
#if !TARGET_OS_TV
|
|
if (context.nextFocusedView != nil) {
|
|
[context.nextFocusedView setAlpha:0.8];
|
|
}
|
|
[context.previouslyFocusedView setAlpha:1.0];
|
|
#endif
|
|
}
|
|
|
|
@end
|