mirror of
https://github.com/moonlight-stream/moonlight-ios.git
synced 2025-07-01 15:26:11 +00:00
399 lines
14 KiB
Objective-C
399 lines
14 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"
|
|
|
|
#include <sys/socket.h>
|
|
#include <netinet/in.h>
|
|
#include <arpa/inet.h>
|
|
#include <Limelight.h>
|
|
|
|
@implementation StreamFrameViewController {
|
|
ControllerSupport *_controllerSupport;
|
|
StreamManager *_streamMan;
|
|
NSTimer *_inactivityTimer;
|
|
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];
|
|
|
|
#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)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];
|
|
[_overlayView setTextAlignment:NSTextAlignmentCenter];
|
|
[_overlayView setTextColor:[OSColor lightGrayColor]];
|
|
[_overlayView setBackgroundColor:[OSColor 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) {
|
|
[_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 {
|
|
[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 immediately on tvOS
|
|
Log(LOG_I, @"Terminating stream after resigning active");
|
|
[self returnToMainFrame];
|
|
#else
|
|
// Terminate the stream if the app is inactive for 10 seconds
|
|
Log(LOG_I, @"Starting inactivity termination timer");
|
|
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:10
|
|
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];
|
|
});
|
|
}
|
|
|
|
- (void)connectionTerminated:(long)errorCode {
|
|
Log(LOG_I, @"Connection terminated: %ld", errorCode);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Allow the display to go to sleep now
|
|
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
|
if (errorCode == 0) {
|
|
[self returnToMainFrame];
|
|
} else {
|
|
UIAlertController* conTermAlert = [UIAlertController alertControllerWithTitle:@"Connection Terminated" message:@"The connection was terminated" preferredStyle:UIAlertControllerStyleAlert];
|
|
[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:(long)errorCode {
|
|
Log(LOG_I, @"Stage %s failed: %ld", stageName, errorCode);
|
|
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
// Allow the display to go to sleep now
|
|
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
|
|
|
UIAlertController* alert = [UIAlertController alertControllerWithTitle:@"Connection Failed"
|
|
message:[NSString stringWithFormat:@"%s failed with error %ld",
|
|
stageName, errorCode]
|
|
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);
|
|
|
|
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)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;
|
|
}
|
|
#endif
|
|
|
|
@end
|