Reformat Metal files Rewrite Metal render loop to not use DisplayLink so we can use the amazing afterMinimumDuration API call Improve frame drop algorithm Misc fixes to HDR <-> SDR switching, deprecation warning cleanup Fix AVSB mode Fix renderingBackend selected value Add Rendering Backend setting to iPhone Let users choose which renderer to use. Refactor metrics into a singleton Fix tvOS build Port moonlight-qt's Metal renderer Bump imgui to latest Misc tweaks and refactoring Switch iOS to use Opus float32 audio, in line with other clients. It was already being sent from the server as float32, so this is simply decoding it with less loss of dynamic range. Give FrameQueue a faster data structure and full test coverage. Remove pts frame pacing, it's not a fit for iOS. Don't allow very transparent opacity for middle stats overlay box Change wording to select button instead of remote button Add tvOS gesture for double-clicking remote button to show/hide stats Remove kVTDecodeFrame_1xRealTimePlayback, this was found to seriously impact decode times Move decode time stat out of async block Enabe FQ logging Misc cleanup/fixes iPad: 26 will require UIRequiresFullScreen to be false, and for all orientations to be supported Updated iPhone and iPad settings panels Add setting for graph opacity. Enable stats and graphs to be toggled via two finger swipe Fix wait time in displaylink, fix settings spacing, stats fixes Settings for frame queue size, graphs. Misc refactoring and cleanup Bump moonlight-common-c Improve logging readability Fix host frametime and frame queue stats to be recorded during input not output. Show min/max/avg decode time Add bytes per frame graph Started on settings changes, both modes working very well Misc refactoring, move graphs around, both pacing functions working well Set frameDropTarget to 2 always Use raw RTP timestamps without converting to floating point for even more accurate playback Standard frame pacing: remove +3 logic Switch to NSRunLoopCommonModes for displayLink Add frame pacing algorithm based on moonlight-qt's Pacer class as a simpler baseline Switch to os_unfair_lock, misc tweaks Large refactor that now uses VTDecompressionSession based on PR535 from felipejfc. This now allows more control over queue size and allows dropping arbitrary frames, but introduces a lot of new problems bump moonlight-common-c to fix 32-bit rollover issue LiWait timing graph Add displaylink interval graph Graphs checkpoint ImGui graph working checkpoint ImGui working checkpoint Add pending frames to stats, add a define for verbose logging DisplayLink WIP Add imgui & implot modules Frame pacing: alternate method based on thieman's pullFrames thread Frame pacing: simplify displayLink, support passing targetTimestamp down to the sample buffer Point to my common-c with some timing changes. Add stats gestures: 2-finger swipe down to show stats, and 2-finger swipe up to hide Limit refresh rate to 60 when in low power mode Display screen refresh rate in stats overlay, remove decode time and explain why it can't be measured Adjust the refresh rate of the display to match the stream framerate and improve battery life Don't use presentationTimeMs when creating CMSampleTimingInfo Fix overlayView build for iOS Refactor CADisplayLink callback for improved frame delivery. Support a user-configurable buffered frame count. [opt] Use UILabel for stats overlay
715 lines
28 KiB
Objective-C
715 lines
28 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>
|
|
|
|
#if TARGET_OS_TV
|
|
#import <AVFoundation/AVDisplayCriteria.h>
|
|
#import <AVKit/AVDisplayManager.h>
|
|
#import <AVKit/UIWindow.h>
|
|
#endif
|
|
|
|
@interface AVDisplayCriteria()
|
|
@property(readonly) int videoDynamicRange;
|
|
@property(readonly, nonatomic) float refreshRate;
|
|
- (id)initWithRefreshRate:(float)arg1 videoDynamicRange:(int)arg2;
|
|
@end
|
|
|
|
@implementation StreamFrameViewController {
|
|
ControllerSupport *_controllerSupport;
|
|
StreamManager *_streamMan;
|
|
TemporarySettings *_settings;
|
|
NSTimer *_inactivityTimer;
|
|
NSTimer *_statsUpdateTimer;
|
|
UITapGestureRecognizer *_menuTapGestureRecognizer;
|
|
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
|
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
|
UITextView *_overlayView;
|
|
UILabel *_stageLabel;
|
|
UILabel *_tipLabel;
|
|
UIActivityIndicatorView *_spinner;
|
|
StreamView *_streamView;
|
|
UIScrollView *_scrollView;
|
|
BOOL _userIsInteracting;
|
|
CGSize _keyboardSize;
|
|
|
|
#if !TARGET_OS_TV
|
|
UIScreenEdgePanGestureRecognizer *_exitSwipeRecognizer;
|
|
#endif
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
- (void)controllerPlayPauseButtonPressed:(id)sender {
|
|
Log(LOG_I, @"Play/Pause button pressed -- backing out of stream");
|
|
[self returnToMainFrame];
|
|
}
|
|
#endif
|
|
|
|
|
|
- (void)viewDidLoad
|
|
{
|
|
[super viewDidLoad];
|
|
|
|
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
|
|
|
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
|
|
|
_settings = [[[DataManager alloc] init] getSettings];
|
|
|
|
_stageLabel = [[UILabel alloc] init];
|
|
[_stageLabel setUserInteractionEnabled:NO];
|
|
[_stageLabel setText:[NSString stringWithFormat:@"Starting %@...", self.streamConfig.appName]];
|
|
[_stageLabel sizeToFit];
|
|
_stageLabel.textAlignment = NSTextAlignmentCenter;
|
|
_stageLabel.textColor = [UIColor whiteColor];
|
|
_stageLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2);
|
|
|
|
_spinner = [[UIActivityIndicatorView alloc] init];
|
|
[_spinner setUserInteractionEnabled:NO];
|
|
#if TARGET_OS_TV
|
|
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhiteLarge];
|
|
#else
|
|
[_spinner setActivityIndicatorViewStyle:UIActivityIndicatorViewStyleWhite];
|
|
#endif
|
|
[_spinner sizeToFit];
|
|
[_spinner startAnimating];
|
|
_spinner.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height / 2 - _stageLabel.frame.size.height - _spinner.frame.size.height);
|
|
|
|
_controllerSupport = [[ControllerSupport alloc] initWithConfig:self.streamConfig delegate:self];
|
|
_inactivityTimer = nil;
|
|
|
|
_streamView = [[StreamView alloc] initWithFrame:self.view.frame];
|
|
[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
|
|
|
|
#if TARGET_OS_TV
|
|
if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
|
|
_menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
|
_menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
|
|
|
_playPauseTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPlayPauseButtonPressed:)];
|
|
_playPauseTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypePlayPause)];
|
|
|
|
_menuDoubleTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonDoublePressed:)];
|
|
_menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2;
|
|
[_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer];
|
|
_menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)];
|
|
}
|
|
|
|
[self.view addGestureRecognizer:_menuTapGestureRecognizer];
|
|
[self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer];
|
|
[self.view addGestureRecognizer:_playPauseTapGestureRecognizer];
|
|
|
|
#else
|
|
_exitSwipeRecognizer = [[UIScreenEdgePanGestureRecognizer alloc] initWithTarget:self action:@selector(edgeSwiped)];
|
|
_exitSwipeRecognizer.edges = UIRectEdgeLeft;
|
|
_exitSwipeRecognizer.delaysTouchesBegan = NO;
|
|
_exitSwipeRecognizer.delaysTouchesEnded = NO;
|
|
|
|
[self.view addGestureRecognizer:_exitSwipeRecognizer];
|
|
#endif
|
|
|
|
_tipLabel = [[UILabel alloc] init];
|
|
[_tipLabel setUserInteractionEnabled:NO];
|
|
|
|
#if TARGET_OS_TV
|
|
[_tipLabel setText:@"Tip: Tap the Play/Pause button on the Apple TV Remote to disconnect from your PC"];
|
|
#else
|
|
[_tipLabel setText:@"Tip: Swipe from the left edge to disconnect from your PC"];
|
|
#endif
|
|
|
|
[_tipLabel sizeToFit];
|
|
_tipLabel.textColor = [UIColor whiteColor];
|
|
_tipLabel.textAlignment = NSTextAlignmentCenter;
|
|
_tipLabel.center = CGPointMake(self.view.frame.size.width / 2, self.view.frame.size.height * 0.9);
|
|
|
|
_streamMan = [[StreamManager alloc] initWithConfig:self.streamConfig
|
|
renderView:_streamView
|
|
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];
|
|
|
|
#if 0
|
|
// FIXME: This doesn't work reliably on iPad for some reason. Showing and hiding the keyboard
|
|
// several times in a row will not correctly restore the state of the UIScrollView.
|
|
[[NSNotificationCenter defaultCenter] addObserver: self
|
|
selector: @selector(keyboardWillShow:)
|
|
name: UIKeyboardWillShowNotification
|
|
object: nil];
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver: self
|
|
selector: @selector(keyboardWillHide:)
|
|
name: UIKeyboardWillHideNotification
|
|
object: nil];
|
|
#endif
|
|
|
|
// Only enable scroll and zoom in absolute touch mode
|
|
if (_settings.absoluteTouchMode) {
|
|
_scrollView = [[UIScrollView alloc] initWithFrame:self.view.frame];
|
|
#if !TARGET_OS_TV
|
|
[_scrollView.panGestureRecognizer setMinimumNumberOfTouches:2];
|
|
#endif
|
|
[_scrollView setShowsHorizontalScrollIndicator:NO];
|
|
[_scrollView setShowsVerticalScrollIndicator:NO];
|
|
[_scrollView setDelegate:self];
|
|
[_scrollView setMaximumZoomScale:10.0f];
|
|
|
|
// Add StreamView inside a UIScrollView for absolute mode
|
|
[_scrollView addSubview:_streamView];
|
|
[self.view addSubview:_scrollView];
|
|
}
|
|
else {
|
|
// Add StreamView directly in relative mode
|
|
[self.view addSubview:_streamView];
|
|
}
|
|
|
|
[self.view addSubview:_stageLabel];
|
|
[self.view addSubview:_spinner];
|
|
[self.view addSubview:_tipLabel];
|
|
}
|
|
|
|
- (UIView *)viewForZoomingInScrollView:(UIScrollView *)scrollView {
|
|
return _streamView;
|
|
}
|
|
|
|
- (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];
|
|
}
|
|
}
|
|
|
|
#if 0
|
|
- (void)keyboardWillShow:(NSNotification *)notification {
|
|
_keyboardSize = [[[notification userInfo] objectForKey:UIKeyboardFrameBeginUserInfoKey] CGRectValue].size;
|
|
|
|
[UIView animateWithDuration:0.3 animations:^{
|
|
CGRect frame = self->_scrollView.frame;
|
|
frame.size.height -= self->_keyboardSize.height;
|
|
self->_scrollView.frame = frame;
|
|
}];
|
|
}
|
|
|
|
-(void)keyboardWillHide:(NSNotification *)notification {
|
|
// NOTE: UIKeyboardFrameEndUserInfoKey returns a different keyboard size
|
|
// than UIKeyboardFrameBeginUserInfoKey, so it's unsuitable for use here
|
|
// to undo the changes made by keyboardWillShow.
|
|
|
|
[UIView animateWithDuration:0.3 animations:^{
|
|
CGRect frame = self->_scrollView.frame;
|
|
frame.size.height += self->_keyboardSize.height;
|
|
self->_scrollView.frame = frame;
|
|
}];
|
|
}
|
|
#endif
|
|
|
|
- (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 {
|
|
// Reset display mode back to default
|
|
[self updatePreferredDisplayMode:NO];
|
|
|
|
[_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];
|
|
|
|
[self->_controllerSupport connectionEstablished];
|
|
|
|
if (self->_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 portFlags = LiGetPortFlagsFromTerminationErrorCode(errorCode);
|
|
unsigned int portTestResults = LiTestClientConnectivity(CONN_TEST_SERVER, 443, portFlags);
|
|
|
|
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.";
|
|
if (portFlags != 0) {
|
|
char failingPorts[256];
|
|
LiStringifyPortFlags(portFlags, "\n", failingPorts, sizeof(failingPorts));
|
|
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
|
}
|
|
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;
|
|
|
|
case ML_ERROR_UNEXPECTED_EARLY_TERMINATION:
|
|
case ML_ERROR_PROTECTED_CONTENT:
|
|
title = @"Connection Error";
|
|
message = @"Something went wrong on your host PC when starting the stream.\n\nMake sure you don't have any DRM-protected content open on your host PC. You can also try restarting your host PC.\n\nIf the issue persists, try reinstalling your GPU drivers and GeForce Experience.";
|
|
break;
|
|
|
|
case ML_ERROR_FRAME_CONVERSION:
|
|
title = @"Connection Error";
|
|
message = @"The host PC reported a fatal video encoding error.\n\nTry disabling HDR mode, changing the streaming resolution, or changing your host PC's display resolution.";
|
|
break;
|
|
|
|
default:
|
|
{
|
|
NSString* errorString;
|
|
if (abs(errorCode) > 1000) {
|
|
// We'll assume large errors are hex values
|
|
errorString = [NSString stringWithFormat:@"%08X", (uint32_t)errorCode];
|
|
}
|
|
else {
|
|
// Smaller values will just be printed as decimal (probably errno.h values)
|
|
errorString = [NSString stringWithFormat:@"%d", errorCode];
|
|
}
|
|
|
|
title = @"Connection Terminated";
|
|
message = [NSString stringWithFormat: @"The connection was terminated\n\nError code: %@", errorString];
|
|
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 (portTestFlags != 0) {
|
|
char failingPorts[256];
|
|
LiStringifyPortFlags(portTestFlags, "\n", failingPorts, sizeof(failingPorts));
|
|
message = [message stringByAppendingString:[NSString stringWithFormat:@"\n\nCheck your firewall and port forwarding rules for port(s):\n%s", failingPorts]];
|
|
}
|
|
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) rumbleTriggers:(uint16_t)controllerNumber leftTrigger:(uint16_t)leftTrigger rightTrigger:(uint16_t)rightTrigger {
|
|
Log(LOG_I, @"Trigger rumble on gamepad %d: %04x %04x", controllerNumber, leftTrigger, rightTrigger);
|
|
|
|
[_controllerSupport rumbleTriggers:controllerNumber leftTrigger:leftTrigger rightTrigger:rightTrigger];
|
|
}
|
|
|
|
- (void) setMotionEventState:(uint16_t)controllerNumber motionType:(uint8_t)motionType reportRateHz:(uint16_t)reportRateHz {
|
|
Log(LOG_I, @"Set motion state on gamepad %d: %02x %u Hz", controllerNumber, motionType, reportRateHz);
|
|
|
|
[_controllerSupport setMotionEventState:controllerNumber motionType:motionType reportRateHz:reportRateHz];
|
|
}
|
|
|
|
- (void) setControllerLed:(uint16_t)controllerNumber r:(uint8_t)r g:(uint8_t)g b:(uint8_t)b {
|
|
Log(LOG_I, @"Set controller LED on gamepad %d: l%02x%02x%02x", controllerNumber, r, g, b);
|
|
|
|
[_controllerSupport setControllerLed:controllerNumber r:r g:g b:b];
|
|
}
|
|
|
|
- (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) updatePreferredDisplayMode:(BOOL)streamActive {
|
|
#if TARGET_OS_TV
|
|
if (@available(tvOS 11.2, *)) {
|
|
UIWindow* window = [[[UIApplication sharedApplication] delegate] window];
|
|
AVDisplayManager* displayManager = [window avDisplayManager];
|
|
|
|
// This logic comes from Kodi and MrMC
|
|
if (streamActive) {
|
|
int dynamicRange;
|
|
|
|
if (LiGetCurrentHostDisplayHdrMode()) {
|
|
dynamicRange = 2; // HDR10
|
|
}
|
|
else {
|
|
dynamicRange = 0; // SDR
|
|
}
|
|
|
|
AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue]
|
|
videoDynamicRange:dynamicRange];
|
|
displayManager.preferredDisplayCriteria = displayCriteria;
|
|
}
|
|
else {
|
|
// Switch back to the default display mode
|
|
displayManager.preferredDisplayCriteria = nil;
|
|
}
|
|
}
|
|
#endif
|
|
}
|
|
|
|
- (void) setHdrMode:(bool)enabled {
|
|
Log(LOG_I, @"HDR is now: %s", enabled ? "active" : "inactive");
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
[self updatePreferredDisplayMode:YES];
|
|
});
|
|
}
|
|
|
|
- (void) videoContentShown {
|
|
[_spinner stopAnimating];
|
|
[self.view setBackgroundColor:[UIColor blackColor]];
|
|
}
|
|
|
|
- (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) streamExitRequested {
|
|
Log(LOG_I, @"Gamepad combo requested stream exit");
|
|
|
|
[self returnToMainFrame];
|
|
}
|
|
|
|
- (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
|