moonlight-ios/Limelight/ViewControllers/StreamFrameViewController.m
Cameron Gutman 42f29c44e6 Remove OSPortabilityDefs.h
The macOS support that used it has been removed for a while
2020-11-01 16:50:02 -06:00

488 lines
18 KiB
Objective-C

//
// StreamFrameViewController.m
// Moonlight
//
// Created by Diego Waxemberg on 1/18/14.
// Copyright (c) 2015 Moonlight Stream. All rights reserved.
//
#import "StreamFrameViewController.h"
#import "MainFrameViewController.h"
#import "VideoDecoderRenderer.h"
#import "StreamManager.h"
#import "ControllerSupport.h"
#import "DataManager.h"
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <Limelight.h>
@implementation StreamFrameViewController {
ControllerSupport *_controllerSupport;
StreamManager *_streamMan;
NSTimer *_inactivityTimer;
NSTimer *_statsUpdateTimer;
UITapGestureRecognizer *_menuGestureRecognizer;
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
UITextView *_overlayView;
StreamView *_streamView;
BOOL _userIsInteracting;
}
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:animated];
#if !TARGET_OS_TV
[[self revealViewController] setPrimaryViewController:self];
#endif
}
#if TARGET_OS_TV
- (void)controllerPauseButtonPressed:(id)sender { }
- (void)controllerPauseButtonDoublePressed:(id)sender {
Log(LOG_I, @"Menu double-pressed -- backing out of stream");
[self returnToMainFrame];
}
#endif
- (void)viewDidLoad
{
[super viewDidLoad];
[self.navigationController setNavigationBarHidden:YES animated:YES];
[self.stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
[self.stageLabel sizeToFit];
self.stageLabel.textAlignment = NSTextAlignmentCenter;
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
self.spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - self.stageLabel.frame.size.height - self.spinner.frame.size.height);
[UIApplication sharedApplication].idleTimerDisabled = YES;
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig presenceDelegate:self];
_inactivityTimer = nil;
_streamView = (StreamView*)self.view;
[_streamView setupStreamView:_controllerSupport swipeDelegate:self interactionDelegate:self config:self.streamConfig];
#if TARGET_OS_TV
if (!_menuGestureRecognizer || !_menuDoubleTapGestureRecognizer) {
_menuGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
_menuGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
_menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
_menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
[_menuGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
_menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
}
[self.view addGestureRecognizer:_menuGestureRecognizer];
[self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
#endif
#if TARGET_OS_TV
[self.tipLabel setText:@"Tip: Double tap the Menu button to disconnect from your PC"];
#else
[self.tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
#endif
[self.tipLabel sizeToFit];
self.tipLabel.textAlignment = NSTextAlignmentCenter;
self.tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
renderView:self.view
connectionCallbacks:self];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:_streamMan];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(applicationWillResignActive:)
name:UIApplicationWillResignActiveNotification
object:nil];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidBecomeActive:)
name: UIApplicationDidBecomeActiveNotification
object: nil];
[[NSNotificationCenter defaultCenter] addObserver: self
selector: @selector(applicationDidEnterBackground:)
name: UIApplicationDidEnterBackgroundNotification
object: nil];
}
- (void)willMoveToParentViewController:(UIViewController *)parent {
// Only cleanup when we're being destroyed
if (parent == nil) {
[_controllerSupport cleanup];
[UIApplication sharedApplication].idleTimerDisabled = NO;
[_streamMan stopStream];
if (_inactivityTimer != nil) {
[_inactivityTimer invalidate];
_inactivityTimer = nil;
}
[[NSNotificationCenter defaultCenter] removeObserver:self];
}
}
- (void)updateStatsOverlay {
NSString* overlayText = [self->_streamMan getStatsOverlayText];
dispatch_async(dispatch_get_main_queue(), ^{
[self updateOverlayText:overlayText];
});
}
- (void)updateOverlayText:(NSString*)text {
if (_overlayView == nil) {
_overlayView = [[UITextView alloc] init];
#if !TARGET_OS_TV
[_overlayView setEditable:NO];
#endif
[_overlayView setUserInteractionEnabled:NO];
[_overlayView setSelectable:NO];
[_overlayView setScrollEnabled:NO];
// HACK: If not using stats overlay, center the text
if (_statsUpdateTimer == nil) {
[_overlayView setTextAlignment:NSTextAlignmentCenter];
}
[_overlayView setTextColor:[UIColor lightGrayColor]];
[_overlayView setBackgroundColor:[UIColor blackColor]];
#if TARGET_OS_TV
[_overlayView setFont:[UIFont systemFontOfSize:24]];
#else
[_overlayView setFont:[UIFont systemFontOfSize:12]];
#endif
[_overlayView setAlpha:0.5];
[self.view addSubview:_overlayView];
}
if (text != nil) {
// We set our bounds to the maximum width in order to work around a bug where
// sizeToFit interacts badly with the UITextView's line breaks, causing the
// width to get smaller and smaller each time as more line breaks are inserted.
[_overlayView setBounds:CGRectMake(self.view.frame.origin.x,
_overlayView.frame.origin.y,
self.view.frame.size.width,
_overlayView.frame.size.height)];
[_overlayView setText:text];
[_overlayView sizeToFit];
[_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)];
[_overlayView setHidden:NO];
}
else {
[_overlayView setHidden:YES];
}
}
- (void) returnToMainFrame {
[_statsUpdateTimer invalidate];
_statsUpdateTimer = nil;
[self.navigationController popToRootViewControllerAnimated:YES];
}
// This will fire if the user opens control center or gets a low battery message
- (void)applicationWillResignActive:(NSNotification *)notification {
if (_inactivityTimer != nil) {
[_inactivityTimer invalidate];
}
#if !TARGET_OS_TV
// Terminate the stream if the app is inactive for 60 seconds
Log(LOG_I, @"Starting inactivity termination timer");
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
target:self
selector:@selector(inactiveTimerExpired:)
userInfo:nil
repeats:NO];
#endif
}
- (void)inactiveTimerExpired:(NSTimer*)timer {
Log(LOG_I, @"Terminating stream after inactivity");
[self returnToMainFrame];
_inactivityTimer = nil;
}
- (void)applicationDidBecomeActive:(NSNotification *)notification {
// Stop the background timer, since we're foregrounded again
if (_inactivityTimer != nil) {
Log(LOG_I, @"Stopping inactivity timer after becoming active again");
[_inactivityTimer invalidate];
_inactivityTimer = nil;
}
}
// This fires when the home button is pressed
- (void)applicationDidEnterBackground:(UIApplication *)application {
Log(LOG_I, @"Terminating stream immediately for backgrounding");
if (_inactivityTimer != nil) {
[_inactivityTimer invalidate];
_inactivityTimer = nil;
}
[self returnToMainFrame];
}
- (void)edgeSwiped {
Log(LOG_I, @"User swiped to end stream");
[self returnToMainFrame];
}
- (void) connectionStarted {
Log(LOG_I, @"Connection started");
dispatch_async(dispatch_get_main_queue(), ^{
// Leave the spinner spinning until it's obscured by
// the first frame of video.
self.stageLabel.hidden = YES;
self.tipLabel.hidden = YES;
[self->_streamView showOnScreenControls];
TemporarySettings* settings = [[[DataManager alloc] init] getSettings];
if (settings.statsOverlay) {
self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f
target:self
selector:@selector(updateStatsOverlay)
userInfo:nil
repeats:YES];
}
});
}
- (void)connectionTerminated:(int)errorCode {
Log(LOG_I, @"Connection terminated: %d", errorCode);
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443,
LiGetPortFlagsFromTerminationErrorCode(errorCode));
dispatch_async(dispatch_get_main_queue(), ^{
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
NSString* title;
NSString* message;
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
title = @"Connection Error";
message = @"Your device's network connection is blocking Moonlight. Streaming may not work while connected to this network.";
}
else {
switch (errorCode) {
case ML_ERROR_GRACEFUL_TERMINATION:
[self returnToMainFrame];
return;
case ML_ERROR_NO_VIDEO_TRAFFIC:
title = @"Connection Error";
message = @"No video received from host. Check the host PC's firewall and port forwarding rules.";
break;
case ML_ERROR_NO_VIDEO_FRAME:
title = @"Connection Error";
message = @"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection.";
break;
default:
title = @"Connection Terminated";
message = @"The connection was terminated";
break;
}
}
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:title
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:conTermAlert];
[conTermAlert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:conTermAlert animated:YES completion:nil];
});
[_streamMan stopStream];
}
- (void) stageStarting:(const char*)stageName {
Log(LOG_I, @"Starting %s", stageName);
dispatch_async(dispatch_get_main_queue(), ^{
NSString* lowerCase = [NSString stringWithFormat:@"%s in progress...", stageName];
NSString* titleCase = [[[lowerCase substringToIndex:1] uppercaseString] stringByAppendingString:[lowerCase substringFromIndex:1]];
[self.stageLabel setText:titleCase];
[self.stageLabel sizeToFit];
self.stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.stageLabel.center.y);
});
}
- (void) stageComplete:(const char*)stageName {
}
- (void) stageFailed:(const char*)stageName withError:(int)errorCode portTestFlags:(int)portTestFlags {
Log(LOG_I, @"Stage %s failed: %d", stageName, errorCode);
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portTestFlags);
dispatch_async(dispatch_get_main_queue(), ^{
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
NSString* message = [NSString stringWithFormat:@"%s failed with error %d", stageName, errorCode];
if (portTestResults != ML_TEST_RESULT_INCONCLUSIVE && portTestResults != 0) {
message = [message stringByAppendingString:@"\n\nYour device's network connection is blocking Moonlight. Streaming may not work while connected to this network."];
}
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:alert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:alert animated:YES completion:nil];
});
[_streamMan stopStream];
}
- (void) launchFailed:(NSString*)message {
Log(LOG_I, @"Launch failed: %@", message);
dispatch_async(dispatch_get_main_queue(), ^{
// Allow the display to go to sleep now
[UIApplication sharedApplication].idleTimerDisabled = NO;
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Error"
message:message
preferredStyle:UIAlertControllerStyleAlert];
[Utils addHelpOptionToDialog:alert];
[alert addAction:[UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
[self returnToMainFrame];
}]];
[self presentViewController:alert animated:YES completion:nil];
});
}
- (void)rumble:(unsigned short)controllerNumber lowFreqMotor:(unsigned short)lowFreqMotor highFreqMotor:(unsigned short)highFreqMotor {
Log(LOG_I, @"Rumble on gamepad %d: %04x %04x", controllerNumber, lowFreqMotor, highFreqMotor);
[_controllerSupport rumble:controllerNumber lowFreqMotor:lowFreqMotor highFreqMotor:highFreqMotor];
}
- (void)connectionStatusUpdate:(int)status {
Log(LOG_W, @"Connection status update: %d", status);
// The stats overlay takes precedence over these warnings
if (_statsUpdateTimer != nil) {
return;
}
dispatch_async(dispatch_get_main_queue(), ^{
switch (status) {
case CONN_STATUS_OKAY:
[self updateOverlayText:nil];
break;
case CONN_STATUS_POOR:
if (self->_streamConfig.bitRate > 5000) {
[self updateOverlayText:@"Slow connection to PC\nReduce your bitrate"];
}
else {
[self updateOverlayText:@"Poor connection to PC"];
}
break;
}
});
}
- (void)didReceiveMemoryWarning
{
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (void)gamepadPresenceChanged {
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
- (void)mousePresenceChanged {
#if !TARGET_OS_TV
if (@available(iOS 14.0, *)) {
[self setNeedsUpdateOfPrefersPointerLocked];
}
#endif
}
- (void)userInteractionBegan {
// Disable hiding home bar when user is interacting.
// iOS will force it to be shown anyway, but it will
// also discard our edges deferring system gestures unless
// we willingly give up home bar hiding preference.
_userIsInteracting = YES;
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
- (void)userInteractionEnded {
// Enable home bar hiding again if conditions allow
_userIsInteracting = NO;
#if !TARGET_OS_TV
if (@available(iOS 11.0, *)) {
[self setNeedsUpdateOfHomeIndicatorAutoHidden];
}
#endif
}
#if !TARGET_OS_TV
// Require a confirmation when streaming to activate a system gesture
- (UIRectEdge)preferredScreenEdgesDeferringSystemGestures {
return UIRectEdgeAll;
}
- (BOOL)prefersHomeIndicatorAutoHidden {
if ([_controllerSupport getConnectedGamepadCount] > 0 &&
[_streamView getCurrentOscState] == OnScreenControlsLevelOff &&
_userIsInteracting == NO) {
// Autohide the home bar when a gamepad is connected
// and the on-screen controls are disabled. We can't
// do this all the time because any touch on the display
// will cause the home indicator to reappear, and our
// preferredScreenEdgesDeferringSystemGestures will also
// be suppressed (leading to possible errant exits of the
// stream).
return YES;
}
return NO;
}
- (BOOL)shouldAutorotate {
return YES;
}
- (BOOL)prefersPointerLocked {
// Pointer lock breaks the UIKit mouse APIs, which is a problem because
// GCMouse is horribly broken on iOS 14.0 for certain mice. Only lock
// the cursor if there is a GCMouse present.
return [GCMouse mice].count > 0;
}
#endif
@end