refactor: adopt UIScene lifecycle
feat: add initial support for pip feat: add setting toggle for pip fix: let os handle invokation of pip which is more reliable fix: UIScene compatibility with airplay fix: black screen on external display when on app view and streaming fix: incorrect window size when launched directly on external display fix: wrong resolution shown in settings when in stream session Enable gamemode flag for iOS18+ Explicitly enters pip mode when in background
This commit is contained in:
@@ -427,7 +427,7 @@ static NSMutableSet* hostList;
|
||||
while (host.state == StateUnknown) {
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
|
||||
// Don't bother polling if the server is already offline
|
||||
if (host.state == StateOffline) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -437,16 +437,16 @@ static NSMutableSet* hostList;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
HttpManager* hMan = [[HttpManager alloc] initWithHost:host];
|
||||
ServerInfoResponse* serverInfoResp = [[ServerInfoResponse alloc] init];
|
||||
|
||||
|
||||
// Exempt this host from discovery while handling the serverinfo request
|
||||
[self->_discMan pauseDiscoveryForHost:host];
|
||||
[hMan executeRequestSynchronously:[HttpRequest requestForResponse:serverInfoResp withUrlRequest:[hMan newServerInfoRequest:false]
|
||||
fallbackError:401 fallbackRequest:[hMan newHttpServerInfoRequest]]];
|
||||
[self->_discMan resumeDiscoveryForHost:host];
|
||||
|
||||
|
||||
if (![serverInfoResp isStatusOk]) {
|
||||
Log(LOG_W, @"Failed to get server info: %@", serverInfoResp.statusMessage);
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
@@ -454,20 +454,20 @@ static NSMutableSet* hostList;
|
||||
[self hideLoadingFrame:nil];
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
UIAlertController* applistAlert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Connection Failed"]
|
||||
message:serverInfoResp.statusMessage
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
[Utils addHelpOptionToDialog:applistAlert];
|
||||
[applistAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"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 switchToHostView];
|
||||
[[self activeViewController] presentViewController:applistAlert animated:YES completion:nil];
|
||||
}];
|
||||
|
||||
|
||||
host.state = StateOffline;
|
||||
});
|
||||
} else {
|
||||
@@ -568,7 +568,7 @@ static NSMutableSet* hostList;
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (UIViewController*) activeViewController {
|
||||
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
|
||||
|
||||
@@ -613,7 +613,7 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
UIAlertController* longClickAlert = [UIAlertController alertControllerWithTitle:host.name message:message preferredStyle:UIAlertControllerStyleActionSheet];
|
||||
|
||||
|
||||
if (host.state != StateOnline) {
|
||||
/*[longClickAlert addAction:[UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Wake PC"] style:UIAlertActionStyleDefault handler:^(UIAlertAction* action){
|
||||
UIAlertController* wolAlert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"Wake-On-LAN"] message:@"" preferredStyle:UIAlertControllerStyleAlert];
|
||||
@@ -828,7 +828,7 @@ static NSMutableSet* hostList;
|
||||
physicalOutputChannels = 8;
|
||||
Log(LOG_I, @"System-provided spatial audio available, pretending device has %d channels", physicalOutputChannels);
|
||||
}
|
||||
|
||||
|
||||
int numberOfChannels = MIN([streamSettings.audioConfig intValue], physicalOutputChannels);
|
||||
Log(LOG_I, @"Selected number of audio channels %d", numberOfChannels);
|
||||
if (numberOfChannels >= 8) {
|
||||
@@ -1124,9 +1124,9 @@ static NSMutableSet* hostList;
|
||||
// enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable.
|
||||
if(self.settingsExpandedInStreamView) [revealController buttonsInStreaming];
|
||||
else [revealController buttonsNotInStreaming];
|
||||
|
||||
|
||||
[streamFrameViewController setUserInteractionEnabledForStreamView:!_settingsExpandedInStreamView || position == FrontViewPositionLeft];
|
||||
|
||||
|
||||
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.resolutionStack];
|
||||
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.fpsStack];
|
||||
[settingsViewController widget:settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView];
|
||||
@@ -1140,13 +1140,28 @@ static NSMutableSet* hostList;
|
||||
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.citrixX1MouseStack];
|
||||
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.externalDisplayModeStack];
|
||||
[settingsViewController setHidden:_settingsExpandedInStreamView forStack:settingsViewController.audioConfigStack];
|
||||
|
||||
|
||||
}
|
||||
|
||||
- (void)revealController:(SWRevealViewController *)revealController didMoveToPosition:(FrontViewPosition)position {
|
||||
// If we moved back to the center position, we should save the settings
|
||||
SettingsViewController* settingsViewController = (SettingsViewController*)[revealController rearViewController];
|
||||
settingsViewController.mainFrameViewController = self;
|
||||
// enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable.
|
||||
[settingsViewController.resolutionSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.framerateSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController widget:settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.optimizeSettingsSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.audioOnPCSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.codecSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.yuv444Selector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.pipSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.hdrSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.framePacingSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.btMouseSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.externalDisplayModeSelector setEnabled:!self.settingsExpandedInStreamView];
|
||||
[settingsViewController.backToStreamingButton setEnabled:self.settingsExpandedInStreamView];
|
||||
// [settingsViewController.unlockDisplayOrientationSelector setEnabled:!self.settingsExpandedInStreamView && [self isFullScreenRequired]];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
|
||||
|
||||
if (position == FrontViewPositionLeft) {
|
||||
[settingsViewController saveSettings];
|
||||
@@ -1264,8 +1279,8 @@ static NSMutableSet* hostList;
|
||||
|
||||
- (bool)isIPhonePortrait{
|
||||
bool isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
|
||||
bool isPortrait = screenHeightInPoints > screenWidthInPoints;
|
||||
return isIPhone && isPortrait;
|
||||
// return isPortrait;
|
||||
@@ -1280,7 +1295,7 @@ static NSMutableSet* hostList;
|
||||
button.clipsToBounds = YES;
|
||||
|
||||
// 设置图标(SF Symbol)
|
||||
|
||||
|
||||
if (@available(iOS 13.0, *)) {
|
||||
UIImageSymbolConfiguration *config = [UIImageSymbolConfiguration configurationWithPointSize:17 weight:UIImageSymbolWeightMedium];
|
||||
UIImage *image = [UIImage systemImageNamed:@"plus.circle" withConfiguration:config];
|
||||
@@ -1352,14 +1367,14 @@ static NSMutableSet* hostList;
|
||||
|
||||
- (void)setupHostViewTitle{
|
||||
self->hostViewTitleLabel = [[UILabel alloc] init];
|
||||
|
||||
|
||||
hostViewTitleLabel.translatesAutoresizingMaskIntoConstraints = NO;
|
||||
hostViewTitleLabel.numberOfLines = 1;
|
||||
hostViewTitleLabel.font = [UIFont systemFontOfSize:30 weight:UIFontWeightSemibold];
|
||||
hostViewTitleLabel.text = [LocalizationHelper localizedStringForKey:@"Hosts"];
|
||||
// CGFloat labelHeight = 60;
|
||||
|
||||
|
||||
|
||||
hostViewTitleLabel.textColor = [ThemeManager textColor];
|
||||
hostViewTitleLabel.textAlignment = NSTextAlignmentCenter;
|
||||
// hostViewTitleLabel.backgroundColor = [UIColor clearColor];
|
||||
@@ -1409,13 +1424,13 @@ static NSMutableSet* hostList;
|
||||
self->_addHostButton = [self createAddHostButton];
|
||||
self->_helpButton = [self createHelpButton];
|
||||
//[self setupHostViewTitle];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
self.navigationItem.rightBarButtonItems = @[_helpButton, _addHostButton]; // 顺序:右边靠右的是第一个
|
||||
|
||||
// Set the side bar button action. When it's tapped, it'll show the sidebar.
|
||||
|
||||
|
||||
[_settingsButton setTarget:self.revealViewController];
|
||||
[_settingsButton setAction:@selector(revealToggle:)];
|
||||
if (@available(iOS 13.0, *)) {
|
||||
@@ -1427,7 +1442,7 @@ static NSMutableSet* hostList;
|
||||
} else {
|
||||
[_settingsButton setTitle:[LocalizationHelper localizedStringForKey:@"Settings"]];
|
||||
}
|
||||
|
||||
|
||||
// Set the host name button action. When it's tapped, it'll show the host selection view.
|
||||
_upButton = [[UIBarButtonItem alloc] init];
|
||||
[self->_upButton setTitle: [LocalizationHelper localizedStringForKey: @"Select New Host"]];
|
||||
@@ -1499,8 +1514,8 @@ static NSMutableSet* hostList;
|
||||
_boxArtCache = [[NSCache alloc] init];
|
||||
|
||||
//recordedScreenWidth = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
|
||||
|
||||
// deal with scroll host view reload after device orientation change here:
|
||||
bool isLandscape = screenWidthInPoints > screenHeightInPoints;
|
||||
@@ -1526,10 +1541,10 @@ static NSMutableSet* hostList;
|
||||
|
||||
[self updateTitle];
|
||||
// [self.view addSubview:hostScrollView];
|
||||
|
||||
|
||||
// if ([hostList count] == 1) [self hostClicked:[hostList anyObject] view:nil]; // auto click for single host
|
||||
|
||||
|
||||
|
||||
//if([SettingsViewController isLandscapeNow] != _streamConfig.width > _streamConfig.height)
|
||||
//[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app.
|
||||
//[self simulateSettingsButtonPress]; //force expand setting view if orientation changed since last quit from app.
|
||||
@@ -1541,7 +1556,7 @@ static NSMutableSet* hostList;
|
||||
UILongPressGestureRecognizer *longPress = [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleMenuResize:)];
|
||||
[self.view addGestureRecognizer:longPress];
|
||||
|
||||
|
||||
|
||||
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:[self isIPhone]?@"iPhone":@"iPad" bundle:nil];
|
||||
SettingsViewController *viewController = [storyboard instantiateViewControllerWithIdentifier:@"settingsViewController"];
|
||||
// 强制加载视图
|
||||
@@ -1573,9 +1588,9 @@ static NSMutableSet* hostList;
|
||||
CGFloat screenScale = window.screen.scale;
|
||||
CGFloat appWindowWidth = CGRectGetWidth(window.frame) * screenScale;
|
||||
CGFloat appWindowHeight = CGRectGetHeight(window.frame) * screenScale;
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
|
||||
|
||||
if(currentSettings.externalDisplayMode.intValue == 1 && UIScreen.screens.count > 1){
|
||||
CGRect bounds = [UIScreen.screens.lastObject bounds];
|
||||
screenScale = [UIScreen.screens.lastObject scale];
|
||||
@@ -1702,7 +1717,7 @@ static NSMutableSet* hostList;
|
||||
- (void)viewDidAppear:(BOOL)animated
|
||||
{
|
||||
[super viewDidAppear:NO];
|
||||
|
||||
|
||||
// [self setupHostViewTitle];
|
||||
// [self reloadScrollHostView]; //remove this for proper test
|
||||
[self attachWaterMark];
|
||||
@@ -1749,7 +1764,7 @@ static NSMutableSet* hostList;
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
[super viewWillAppear:NO];
|
||||
|
||||
|
||||
/* this makes background color works*/
|
||||
for (UIView *subview in self.view.subviews) {
|
||||
[subview removeFromSuperview]; // 暂时移除所有子视图
|
||||
@@ -1774,11 +1789,11 @@ static NSMutableSet* hostList;
|
||||
[self.view addSubview:self.collectionView];
|
||||
[self initHostCollection];
|
||||
if(!_enteredAppView) [self switchToHostView];
|
||||
|
||||
|
||||
[self retrieveSavedHosts];
|
||||
|
||||
|
||||
_discMan = [[DiscoveryManager alloc] initWithHosts:[hostList allObjects] andCallback:self];
|
||||
|
||||
|
||||
[self beginForegroundRefresh];
|
||||
}
|
||||
|
||||
@@ -1870,7 +1885,7 @@ static NSMutableSet* hostList;
|
||||
for (TemporaryHost* comp in sortedHostList) {
|
||||
if(comp.state == StateOnline || comp.pairState == PairStatePaired) [self.hostCollectionVC addHost:comp];
|
||||
|
||||
|
||||
|
||||
// new host card test
|
||||
// if([comp.name isEqualToString: @"ASRockPC"]) {
|
||||
/*
|
||||
@@ -1878,18 +1893,18 @@ static NSMutableSet* hostList;
|
||||
//if([comp.name isEqualToString: @"PianoServer"]) {
|
||||
//if([comp.name isEqualToString: @"TrueZj"]) {
|
||||
HostCardView *testCard = [[HostCardView alloc] initWithHost:comp];
|
||||
|
||||
|
||||
[self.view addSubview:testCard];
|
||||
// [testCard resizeBySizeFactor:3];
|
||||
[NSLayoutConstraint activateConstraints:@[
|
||||
[testCard.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor constant:0],
|
||||
[testCard.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor constant:150],
|
||||
]];
|
||||
|
||||
|
||||
}*/
|
||||
//
|
||||
|
||||
|
||||
|
||||
|
||||
// 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), ^{
|
||||
@@ -1983,7 +1998,7 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
_sortedAppList = visibleAppList;
|
||||
}
|
||||
|
||||
|
||||
[self.collectionView reloadData];
|
||||
}
|
||||
|
||||
@@ -2065,7 +2080,7 @@ static NSMutableSet* hostList;
|
||||
|
||||
menuSeparator = [self findMenuSeparator];
|
||||
if(menuSeparator.hidden || menuSeparator == nil) return;
|
||||
|
||||
|
||||
if(gesture.state == UIGestureRecognizerStateBegan){
|
||||
if(locationInView.x < 30) {
|
||||
snapshot = [[UIView alloc] init];
|
||||
@@ -2076,9 +2091,9 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
bool isPortrait = screenHeight>screenWidth;
|
||||
|
||||
|
||||
CGFloat limitedWidth = MIN(MAX(locationInSuperView.x, isPortrait ? 200 : 280),isPortrait ? screenWidth*0.75 : screenWidth/2);
|
||||
if(gesture.state == UIGestureRecognizerStateChanged){
|
||||
if(snapshot) snapshot.center = CGPointMake(limitedWidth, snapshot.center.y);
|
||||
@@ -2093,8 +2108,8 @@ static NSMutableSet* hostList;
|
||||
Settings* settings = [dataMan retrieveSettings];
|
||||
settings.settingsMenuWidth = [NSNumber numberWithFloat:self.revealViewController.rearViewRevealWidth];
|
||||
[dataMan saveData];
|
||||
|
||||
|
||||
|
||||
|
||||
double delayInSeconds = 0.02;
|
||||
dispatch_time_t popTime = dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delayInSeconds * NSEC_PER_SEC));
|
||||
dispatch_after(popTime, dispatch_get_main_queue(), ^{
|
||||
@@ -2182,7 +2197,7 @@ static NSMutableSet* hostList;
|
||||
// 添加为子控制器
|
||||
[self addChildViewController:self.hostCollectionVC];
|
||||
[self.view addSubview:self.hostCollectionVC.view];
|
||||
|
||||
|
||||
// 设置其布局(Auto Layout 示例)
|
||||
// CGFloat hostCollectionViewPadding = 75;
|
||||
CGFloat leftPadding = [self isIPhone] ? 30 : 0;
|
||||
@@ -2193,10 +2208,10 @@ static NSMutableSet* hostList;
|
||||
[self.hostCollectionVC.view.trailingAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.trailingAnchor constant:0],
|
||||
// [self.hostCollectionVC.view.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor constant:0] //?
|
||||
]];
|
||||
|
||||
|
||||
// 通知子控制器已添加完成
|
||||
[self.hostCollectionVC didMoveToParentViewController:self];
|
||||
|
||||
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -800,8 +800,8 @@ const int FrontViewPositionNone = 0xff;
|
||||
}
|
||||
|
||||
- (UIInterfaceOrientationMask)getCurrentOrientation{
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
|
||||
CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
|
||||
CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
|
||||
//lock the orientation accordingly after streaming is started
|
||||
if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape;
|
||||
else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown;
|
||||
|
||||
@@ -14,18 +14,23 @@
|
||||
#import "StreamView.h"
|
||||
#import "LayoutOnScreenControlsViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "StreamManager.h"
|
||||
|
||||
#import <AVKit/AVKit.h>
|
||||
#import <UIKit/UIKit.h>
|
||||
|
||||
#if TARGET_OS_TV
|
||||
@import GameController;
|
||||
|
||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate>
|
||||
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, AVPictureInPictureControllerDelegate>
|
||||
#else
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolboxSpecialEntryDelegate>
|
||||
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolboxSpecialEntryDelegate, AVPictureInPictureControllerDelegate>
|
||||
|
||||
#endif
|
||||
@property (nonatomic) StreamManager* streamMan;
|
||||
@property (nonatomic) StreamConfiguration* streamConfig;
|
||||
@property (nonatomic, strong) AVPictureInPictureController *pipController API_AVAILABLE(ios(9.0));
|
||||
@property (nonatomic, strong) AVPictureInPictureControllerContentSource *pipContentSource API_AVAILABLE(ios(15.0)); // Needed for iOS 15+ layer-based PiP
|
||||
@property (nonatomic, assign) MainFrameViewController *mainFrameViewcontroller;
|
||||
|
||||
|
||||
|
||||
@@ -9,10 +9,12 @@
|
||||
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
|
||||
//
|
||||
|
||||
#import <AVKit/AVKit.h>
|
||||
#import "StreamFrameViewController.h"
|
||||
#import "MainFrameViewController.h"
|
||||
#import "VideoDecoderRenderer.h"
|
||||
#import "StreamManager.h"
|
||||
#import "SceneDelegate.h"
|
||||
#import "ControllerSupport.h"
|
||||
#import "DataManager.h"
|
||||
#import "CustomEdgeSlideGestureRecognizer.h"
|
||||
@@ -69,6 +71,8 @@
|
||||
*/
|
||||
UIWindow *_deviceWindow;
|
||||
dispatch_block_t _delayedRemoveExtScreen;
|
||||
VideoDecoderRenderer *_videoRenderer;
|
||||
BOOL _isRestoringFromPiP;
|
||||
#if !TARGET_OS_TV
|
||||
CustomEdgeSlideGestureRecognizer *_slideToSettingsRecognizer;
|
||||
CustomEdgeSlideGestureRecognizer *_slideToCmdToolRecognizer;
|
||||
@@ -76,12 +80,67 @@
|
||||
LayoutOnScreenControlsViewController *_layoutOnScreenControlsVC;
|
||||
ToolboxViewController* toolBoxViewController;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerWillStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
_streamView.hidden = YES;
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerDidStartPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController failedToStartPictureInPictureWithError:(NSError *)error {
|
||||
Log(LOG_E, @"PiP Failed to Start: %@", error);
|
||||
_streamView.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerWillStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
}
|
||||
|
||||
- (void)pictureInPictureControllerDidStopPictureInPicture:(AVPictureInPictureController *)pictureInPictureController {
|
||||
_streamView.hidden = NO;
|
||||
if (!_isRestoringFromPiP) {
|
||||
[self returnToMainFrame];
|
||||
}
|
||||
_isRestoringFromPiP = NO;
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController restoreUserInterfaceForPictureInPictureStopWithCompletionHandler:(void (^)(BOOL restored))completionHandler {
|
||||
_isRestoringFromPiP = YES;
|
||||
_streamView.hidden = NO;
|
||||
completionHandler(YES);
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController
|
||||
didTransitionToRenderSize:(CMVideoDimensions)newRenderSize API_AVAILABLE(ios(15.0)){
|
||||
Log(LOG_I, @"PiP transitioned to size: %d x %d", newRenderSize.width, newRenderSize.height);
|
||||
}
|
||||
|
||||
// Indicate that playback is never paused for a live stream
|
||||
- (BOOL)pictureInPictureControllerIsPlaybackPaused:(AVPictureInPictureController *)pictureInPictureController API_AVAILABLE(ios(14.0)) {
|
||||
return NO;
|
||||
}
|
||||
|
||||
// Return an indefinite time range for a live stream
|
||||
- (CMTimeRange)pictureInPictureControllerTimeRangeForPlayback:(AVPictureInPictureController *)pictureInPictureController API_AVAILABLE(ios(14.0)) {
|
||||
return CMTimeRangeMake(kCMTimeZero, kCMTimePositiveInfinity);
|
||||
}
|
||||
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController setPlaying:(BOOL)playing API_AVAILABLE(ios(14.0)) {
|
||||
}
|
||||
|
||||
// Live streams typically can't skip, so just call the completion handler.
|
||||
- (void)pictureInPictureController:(AVPictureInPictureController *)pictureInPictureController skipByInterval:(CMTime)skipInterval completionHandler:(void (^)(void))completionHandler API_AVAILABLE(ios(14.0)) {
|
||||
if (completionHandler) {
|
||||
completionHandler();
|
||||
}
|
||||
}
|
||||
|
||||
- (BOOL)isFirstLaunch {
|
||||
NSString *key = @"hasLaunchedBefore";
|
||||
BOOL launchedBefore = [[NSUserDefaults standardUserDefaults] boolForKey:key];
|
||||
|
||||
|
||||
if (!launchedBefore) {
|
||||
[[NSUserDefaults standardUserDefaults] setBool:YES forKey:key];
|
||||
[[NSUserDefaults standardUserDefaults] synchronize]; // iOS 12+ 可省略
|
||||
@@ -95,6 +154,52 @@
|
||||
return (_settings.touchMode.intValue == RelativeTouch || _settings.touchMode.intValue == NativeTouch || _settings.touchMode.intValue == AbsoluteTouch) && _settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
|
||||
}
|
||||
|
||||
- (void)setupPiPControllerWithRenderer:(VideoDecoderRenderer *)videoRenderer { // Ensure we have the renderer and its layer
|
||||
if (self.pipController) {
|
||||
return;
|
||||
}
|
||||
Log(LOG_I, @"Setting up PiP controller...");
|
||||
|
||||
if (!videoRenderer || !videoRenderer.displayLayer) {
|
||||
Log(LOG_E, @"PiP setup failed: Video renderer or display layer not ready.");
|
||||
return;
|
||||
}
|
||||
|
||||
AVSampleBufferDisplayLayer *streamLayer = videoRenderer.displayLayer;
|
||||
|
||||
if ([AVPictureInPictureController isPictureInPictureSupported]) {
|
||||
if (@available(iOS 15.0, *)) {
|
||||
self.pipContentSource = [[AVPictureInPictureControllerContentSource alloc] initWithSampleBufferDisplayLayer:streamLayer playbackDelegate:self];
|
||||
self.pipController = [[AVPictureInPictureController alloc] initWithContentSource:self.pipContentSource];
|
||||
self.pipController.canStartPictureInPictureAutomaticallyFromInline = YES;
|
||||
} else {
|
||||
Log(LOG_E, @"PiP not fully supported on this device.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.pipController) {
|
||||
self.pipController.delegate = self;
|
||||
Log(LOG_I, @"PiP controller created successfully.");
|
||||
} else {
|
||||
Log(LOG_E, @"Failed to create PiP controller.");
|
||||
}
|
||||
} else {
|
||||
Log(LOG_E, @"PiP not supported on this device.");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)cleanupPiPController {
|
||||
if (self.pipController) {
|
||||
self.pipController.delegate = nil;
|
||||
self.pipController = nil;
|
||||
if (@available(iOS 15.0, *)) {
|
||||
self.pipContentSource = nil;
|
||||
}
|
||||
|
||||
Log(LOG_I, @"PiP controller cleaned up.");
|
||||
}
|
||||
}
|
||||
|
||||
- (void)configOscLayoutTool{
|
||||
if([self isOscLayoutToolEnabled]){
|
||||
if(![toolBoxViewController.specialEntries containsObject:@"widgetLayoutTool"]) [toolBoxViewController.specialEntries insertObject:@"widgetLayoutTool" atIndex:0];
|
||||
@@ -224,9 +329,9 @@
|
||||
[super viewDidAppear:animated];
|
||||
|
||||
_deviceWindow = self.view.window;
|
||||
|
||||
if (UIScreen.screens.count > 1 && [self isAirPlayEnabled]) {
|
||||
[self prepExtScreen:UIScreen.screens.lastObject];
|
||||
UIScreen *currentScreen = self.view.window.windowScene.screen;
|
||||
if (UIScreen.screens.count > 1 && [self isAirPlayEnabled] && currentScreen == UIScreen.mainScreen) {
|
||||
[SceneDelegate setExternalDisplayRenderView:self->_streamVideoRenderView];
|
||||
}
|
||||
else {
|
||||
/*
|
||||
@@ -239,7 +344,7 @@
|
||||
if(self->_settings.externalDisplayMode.intValue != 2) [self->_streamView insertSubview:self->_streamVideoRenderView atIndex:0];
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
self->_streamView.originalFrame = self->_streamView.frame;
|
||||
|
||||
// check to see if external screen is connected/disconnected
|
||||
@@ -270,17 +375,17 @@
|
||||
selector:@selector(expandSettingsView) // //force expand settings view to update resolution table, and all setting includes current fullscreen resolution will be updated.
|
||||
name:@"SettingsOverlayButtonPressedNotification"
|
||||
object:nil];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillShow:)
|
||||
name:UIKeyboardWillShowNotification
|
||||
object:nil];
|
||||
|
||||
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self
|
||||
selector:@selector(keyboardWillHide)
|
||||
name:UIKeyboardWillHideNotification
|
||||
object:nil];
|
||||
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -300,7 +405,7 @@
|
||||
return; // temp disable
|
||||
// 初始化倒计时秒数
|
||||
__block NSInteger remainingSeconds = 16;
|
||||
|
||||
|
||||
NSString* settingsEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"left"] : [LocalizationHelper localizedStringForKey:@"right"];
|
||||
NSString* cmdToolEdgeSide = _settings.slideToSettingsScreenEdge.intValue == UIRectEdgeLeft ? [LocalizationHelper localizedStringForKey:@"right"] : [LocalizationHelper localizedStringForKey:@"left"];
|
||||
uint8_t slideDist = (uint8_t)(_settings.slideToSettingsDistance.floatValue * 100);
|
||||
@@ -308,7 +413,7 @@
|
||||
UIAlertController *alert = [UIAlertController alertControllerWithTitle:[LocalizationHelper localizedStringForKey:@"First Launch Tips"]
|
||||
message:[LocalizationHelper localizedStringForKey:@"\n1. From %@ edge, slide %d%% screen width to open settings menu\n\n2. From %@ edge, slide %d%% screen width to open command tool\n\n3. Back to Streaming from settings menu: slide from left edge\n\n4. Slide from upper 40%% of screen edge to avoid sending touch events to PC side", settingsEdgeSide, slideDist, cmdToolEdgeSide, slideDist]
|
||||
preferredStyle:UIAlertControllerStyleAlert];
|
||||
|
||||
|
||||
// 添加确认按钮(初始禁用)
|
||||
UIAlertAction *confirmAction = [UIAlertAction actionWithTitle:[LocalizationHelper localizedStringForKey:@"Got it! (15)"]
|
||||
style:UIAlertActionStyleDefault
|
||||
@@ -316,17 +421,17 @@
|
||||
}];
|
||||
confirmAction.enabled = NO;
|
||||
[alert addAction:confirmAction];
|
||||
|
||||
|
||||
// 显示弹窗
|
||||
[self presentViewController:alert animated:YES completion:nil];
|
||||
|
||||
|
||||
// 使用dispatch_source_t实现精确倒计时
|
||||
dispatch_source_t timer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
|
||||
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1.0 * NSEC_PER_SEC, 0.1 * NSEC_PER_SEC);
|
||||
|
||||
|
||||
dispatch_source_set_event_handler(timer, ^{
|
||||
remainingSeconds--;
|
||||
|
||||
|
||||
if (remainingSeconds <= 0) {
|
||||
// 倒计时结束
|
||||
dispatch_source_cancel(timer);
|
||||
@@ -338,9 +443,9 @@
|
||||
[confirmAction setValue:[NSString stringWithFormat:[LocalizationHelper localizedStringForKey:@"Got it! (%ld)", remainingSeconds], (long)remainingSeconds] forKey:@"title"];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
dispatch_resume(timer);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -348,7 +453,7 @@
|
||||
{
|
||||
viewJustLoaded = true;
|
||||
[super viewDidLoad];
|
||||
|
||||
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
@@ -381,7 +486,9 @@
|
||||
|
||||
toolBoxViewController = [[ToolboxViewController alloc] init];
|
||||
toolBoxViewController.specialEntryDelegate = self;
|
||||
|
||||
|
||||
_isRestoringFromPiP = NO;
|
||||
|
||||
/*
|
||||
_settings.externalDisplayMode.intValue:
|
||||
0 - stage manager
|
||||
@@ -399,7 +506,7 @@
|
||||
[self reConfigStreamViewRealtime]; // call this method again to make sure all gestures are configured & added to the superview(self.view), including the gestures added from inside the streamview.
|
||||
|
||||
if([self isFirstLaunch]) [self popFirstLaunchTip];
|
||||
|
||||
|
||||
#if TARGET_OS_TV
|
||||
if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) {
|
||||
_menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)];
|
||||
@@ -541,7 +648,7 @@
|
||||
if (parent == nil) {
|
||||
//NSLog(@"gyro cleanup, count: %ld", _controller.count);
|
||||
[_controllerSupport cleanup];
|
||||
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
[_streamMan stopStream];
|
||||
if (_inactivityTimer != nil) {
|
||||
@@ -642,7 +749,12 @@
|
||||
- (void) returnToMainFrame {
|
||||
// Reset display mode back to default
|
||||
[self updatePreferredDisplayMode:NO];
|
||||
|
||||
[SceneDelegate clearExternalDisplayRenderView];
|
||||
|
||||
if (_settings.enablePIP) {
|
||||
[self cleanupPiPController];
|
||||
}
|
||||
|
||||
[_statsUpdateTimer invalidate];
|
||||
_statsUpdateTimer = nil;
|
||||
|
||||
@@ -654,19 +766,34 @@
|
||||
// External Screen connected
|
||||
- (void)extScreenDidConnect:(NSNotification *)notification {
|
||||
Log(LOG_I, @"External Screen Connected");
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self prepExtScreen:notification.object];
|
||||
});
|
||||
if ([self isAirPlayEnabled] && [notification.object isKindOfClass:[UIScreen class]]) {
|
||||
UIScreen *extScreen = (UIScreen *)notification.object;
|
||||
if (_streamVideoRenderView) {
|
||||
// Remove from current superview before passing it
|
||||
[_streamVideoRenderView removeFromSuperview];
|
||||
[SceneDelegate setExternalDisplayRenderView:_streamVideoRenderView];
|
||||
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:@"ScreenChanged" object:self];
|
||||
} else {
|
||||
Log(LOG_W, @"_streamVideoRenderView is nil when external screen connected.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External Screen disconnected
|
||||
- (void)extScreenDidDisconnect:(NSNotification *)notification {
|
||||
Log(LOG_I, @"External Screen Disconnected");
|
||||
if(UIScreen.screens.count < 2)
|
||||
{
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[self removeExtScreen];
|
||||
});
|
||||
if(UIScreen.screens.count < 2) {
|
||||
[SceneDelegate clearExternalDisplayRenderView];
|
||||
// Add the render view back to the local StreamView if AirPlay was active
|
||||
if ([self isAirPlayEnabled]) {
|
||||
if (_streamVideoRenderView && _streamView) {
|
||||
[_streamView insertSubview:_streamVideoRenderView atIndex:0];
|
||||
[self handleViewResize]; // Adjust frames as needed
|
||||
}
|
||||
}
|
||||
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:@"ScreenChanged" object:self]; // Your existing notification
|
||||
}
|
||||
}
|
||||
|
||||
@@ -675,7 +802,10 @@
|
||||
}
|
||||
|
||||
- (BOOL) isAirPlaying{
|
||||
return _extWindow != nil && _extWindow.hidden == NO;
|
||||
if (_settings.externalDisplayMode.intValue == 1 && _streamVideoRenderView) {
|
||||
return _streamVideoRenderView.hidden;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL) isAirPlayEnabled{
|
||||
@@ -685,45 +815,12 @@
|
||||
- (void) reloadAirPlayConfig{
|
||||
if (UIScreen.screens.count == 1){return;}
|
||||
if (![self isAirPlaying] && [self isAirPlayEnabled]){
|
||||
[self prepExtScreen:UIScreen.screens.lastObject];
|
||||
[SceneDelegate setExternalDisplayRenderView:_streamVideoRenderView];
|
||||
}else if ([self isAirPlaying] && ![self isAirPlayEnabled]){
|
||||
[self removeExtScreen];
|
||||
[SceneDelegate clearExternalDisplayRenderView];
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare Screen
|
||||
- (void)prepExtScreen:(UIScreen*)extScreen {
|
||||
Log(LOG_I, @"Preparing External Screen");
|
||||
if(![self isAirPlayEnabled]){
|
||||
return;
|
||||
}
|
||||
CGRect frame = extScreen.bounds;
|
||||
extScreen.overscanCompensation = UIScreenOverscanCompensationNone;
|
||||
if(_extWindow==nil){
|
||||
_extWindow = [[UIWindow alloc] initWithFrame:frame];
|
||||
}
|
||||
_extWindow.screen = extScreen;
|
||||
_streamVideoRenderView.bounds = frame;
|
||||
_streamVideoRenderView.frame = frame;
|
||||
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
|
||||
[nc postNotificationName:@"ScreenChanged" object:self];
|
||||
[_extWindow addSubview:_streamVideoRenderView];
|
||||
_extWindow.hidden = NO;
|
||||
}
|
||||
|
||||
- (void)removeExtScreen {
|
||||
Log(LOG_I, @"Removing External Screen");
|
||||
_extWindow.hidden = YES;
|
||||
[self handleViewResize];
|
||||
/*
|
||||
_settings.externalDisplayMode.intValue:
|
||||
0 - stage manager
|
||||
1 - airplay
|
||||
2 - disabled
|
||||
*/
|
||||
if(_settings.externalDisplayMode.intValue != 2) [_streamView insertSubview:_streamVideoRenderView atIndex:0];
|
||||
}
|
||||
|
||||
- (void) handleViewResize{
|
||||
_streamView.bounds = _deviceWindow.bounds;
|
||||
_streamView.frame = _deviceWindow.frame;
|
||||
@@ -739,8 +836,18 @@
|
||||
|
||||
// This will fire if the user opens control center or gets a low battery message
|
||||
- (void)applicationWillResignActive:(NSNotification *)notification {
|
||||
if (_settings.enablePIP && self.pipController && !self.pipController.isPictureInPictureActive && self.pipController.isPictureInPicturePossible) {
|
||||
VideoDecoderRenderer *renderer = self.streamMan.videoRenderer;
|
||||
if (renderer && renderer.displayLayer && self.view.window) {
|
||||
Log(LOG_I, @"Starting Picture in Picture");
|
||||
[self.pipController startPictureInPicture];
|
||||
} else {
|
||||
Log(LOG_I, @"Skipping PiP start: Renderer/Layer/Window not ready OR PiP not possible.");
|
||||
}
|
||||
}
|
||||
if (_inactivityTimer != nil) {
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
|
||||
#if !TARGET_OS_TV
|
||||
@@ -769,11 +876,14 @@
|
||||
[_inactivityTimer invalidate];
|
||||
_inactivityTimer = nil;
|
||||
}
|
||||
if (self.pipController && self.pipController.isPictureInPictureActive) {
|
||||
[self.pipController stopPictureInPicture];
|
||||
}
|
||||
}
|
||||
|
||||
// This fires when the home button is pressed
|
||||
- (void)applicationDidEnterBackground:(UIApplication *)application {
|
||||
|
||||
|
||||
Log(LOG_I, @"Terminating stream immediately for backgrounding");
|
||||
|
||||
if (_inactivityTimer != nil) {
|
||||
@@ -782,6 +892,21 @@
|
||||
}
|
||||
|
||||
// [self returnToMainFrame];
|
||||
// Don't terminate if pip is active
|
||||
if (_settings.enablePIP && self.pipController && self.pipController.isPictureInPictureActive) {
|
||||
Log(LOG_I, @"PIP is active, not terminating stream");
|
||||
return;
|
||||
}
|
||||
|
||||
#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)expandSettingsView{
|
||||
@@ -1059,8 +1184,21 @@
|
||||
}
|
||||
|
||||
- (void) videoContentShown {
|
||||
[_spinner stopAnimating];
|
||||
[self.view setBackgroundColor:[UIColor blackColor]];
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
[_spinner stopAnimating];
|
||||
[self.view setBackgroundColor:[UIColor blackColor]];
|
||||
|
||||
if (@available(iOS 15.0, *)) {
|
||||
if (_settings.enablePIP) {
|
||||
if (self->_streamMan && self->_streamMan.videoRenderer) {
|
||||
Log(LOG_I, @"Setting up PiP with renderer: %p", self->_streamMan.videoRenderer);
|
||||
[self setupPiPControllerWithRenderer:self->_streamMan.videoRenderer];
|
||||
} else {
|
||||
Log(LOG_I, @"No renderer available for PiP setup");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
- (void)didReceiveMemoryWarning
|
||||
|
||||
Reference in New Issue
Block a user