Merge branch 'piptest' into Integration

- PiP & its toolbox special entry
- host update timer

# Conflicts:
#	VoidLink/ViewControllers/MainFrameViewController.m
#	VoidLink/ViewControllers/StreamFrameViewController.m
This commit is contained in:
TrueZhuanjia
2025-07-12 16:41:47 +08:00
36 changed files with 655 additions and 196 deletions
@@ -429,7 +429,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(), ^{
@@ -439,16 +439,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(), ^{
@@ -456,20 +456,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 {
@@ -570,7 +570,7 @@ static NSMutableSet* hostList;
}];
}
- (UIViewController*) activeViewController {
UIViewController *topController = [UIApplication sharedApplication].keyWindow.rootViewController;
@@ -615,7 +615,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];
@@ -830,7 +830,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) {
@@ -1126,9 +1126,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];
@@ -1142,7 +1142,7 @@ 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 {
@@ -1282,7 +1282,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];
@@ -1354,14 +1354,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];
@@ -1411,13 +1411,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, *)) {
@@ -1429,7 +1429,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,20 +1499,6 @@ static NSMutableSet* hostList;
}
_boxArtCache = [[NSCache alloc] init];
//recordedScreenWidth = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]);
// deal with scroll host view reload after device orientation change here:
bool isLandscape = screenWidthInPoints > screenHeightInPoints;
CGFloat realViewFrameWidth = self.view.frame.size.width > self.view.frame.size.height ? self.view.frame.size.width : self.view.frame.size.height;
CGFloat realViewFrameHeight = self.view.frame.size.width < self.view.frame.size.height ? self.view.frame.size.width : self.view.frame.size.height;
if(!isLandscape) {
CGFloat tmpLength = realViewFrameWidth;
realViewFrameWidth = realViewFrameHeight;
realViewFrameHeight = tmpLength;
}
self.collectionView.delaysContentTouches = NO;
@@ -1528,10 +1514,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.
@@ -1543,7 +1529,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"];
// 强制加载视图
@@ -1709,7 +1695,7 @@ static NSMutableSet* hostList;
- (void)viewDidAppear:(BOOL)animated
{
[super viewDidAppear:NO];
// [self setupHostViewTitle];
// [self reloadScrollHostView]; //remove this for proper test
[self attachWaterMark];
@@ -1761,7 +1747,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]; // 暂时移除所有子视图
@@ -1786,11 +1772,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];
}
@@ -1880,6 +1866,7 @@ static NSMutableSet* hostList;
// Sort the host list in alphabetical order
NSArray* sortedHostList = [[hostList allObjects] sortedArrayUsingSelector:@selector(compareName:)];
for (TemporaryHost* comp in sortedHostList) {
if(comp.state == StateOnline || comp.pairState == PairStatePaired || comp.pairState == PairStateUnknown) [self.hostCollectionVC addHost:comp];
// new host card test
@@ -1889,18 +1876,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), ^{
@@ -1994,7 +1981,7 @@ static NSMutableSet* hostList;
}
_sortedAppList = visibleAppList;
}
[self.collectionView reloadData];
}
@@ -2076,7 +2063,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];
@@ -2087,9 +2074,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);
@@ -2104,8 +2091,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(), ^{
@@ -2193,7 +2180,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;
@@ -2204,10 +2191,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;
@@ -47,6 +47,7 @@
@property (strong, nonatomic) IBOutlet UISwitch *audioOnPcSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *codecStack;
@property (strong, nonatomic) IBOutlet UIStackView *yuv444Stack;
@property (strong, nonatomic) IBOutlet UIStackView *pipStack;
@property (strong, nonatomic) IBOutlet UIStackView *HdrStack;
@property (strong, nonatomic) IBOutlet UIStackView *framepacingStack;
@property (strong, nonatomic) IBOutlet UIStackView *reverseMouseWheelDirectionStack;
@@ -72,6 +73,7 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
@property (strong, nonatomic) IBOutlet UISwitch *hdrSwitch;
@property (strong, nonatomic) IBOutlet UISwitch *yuv444Switch;
@property (strong, nonatomic) IBOutlet UISwitch *pipSwitch;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *reverseMouseWheelDirectionSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *statsOverlaySelector;
@@ -253,7 +253,7 @@ BOOL isCustomResolution(CGSize res) {
}
- (void)updateResolutionTable{
UIWindow *window = UIApplication.sharedApplication.windows.firstObject;
UIWindow *window = self.view.window;
CGFloat screenScale = window.screen.scale;
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
CGFloat appWindowWidth = window.frame.size.width * screenScale;
@@ -482,6 +482,7 @@ BOOL isCustomResolution(CGSize res) {
[self addSetting:self.codecStack ofId:@"codecStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
[self addSetting:self.HdrStack ofId:@"HdrStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
[self addSetting:self.yuv444Stack ofId:@"yuv444Stack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[self addSetting:self.pipStack ofId:@"pipStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
[videoSection addToParentStack:_parentStack];
[videoSection setExpanded:YES];
@@ -1245,6 +1246,7 @@ BOOL isCustomResolution(CGSize res) {
}
[self.yuv444Switch setOn:currentSettings.enableYUV444];
[self.pipSwitch setOn:currentSettings.enablePIP];
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
[self.citrixX1MouseSwitch setOn:currentSettings.btMouseSupport];
[self.optimizeGamesSwitch setOn: currentSettings.optimizeGames];
@@ -1957,6 +1959,7 @@ BOOL isCustomResolution(CGSize res) {
BOOL audioOnPC = self.audioOnPcSwitch.isOn;
uint32_t preferredCodec = [self getChosenCodecPreference];
BOOL enableYUV444 = self.yuv444Switch.isOn;
BOOL enablePIP = self.pipSwitch.isOn;
BOOL btMouseSupport = self.citrixX1MouseSwitch.isOn;
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
NSInteger touchMode = [self isNotNativeTouchOnly] ? self.touchModeSelector.selectedSegmentIndex : NativeTouchOnly;
@@ -1993,6 +1996,7 @@ BOOL isCustomResolution(CGSize res) {
audioOnPC:audioOnPC
preferredCodec:preferredCodec
enableYUV444:enableYUV444
enablePIP:enablePIP
useFramePacing:useFramePacing
enableHdr:enableHdr
btMouseSupport:btMouseSupport
@@ -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,10 +154,71 @@
return (_settings.touchMode.intValue == RelativeTouch || _settings.touchMode.intValue == NativeTouch || _settings.touchMode.intValue == AbsoluteTouch) && _settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
}
- (void)configOscLayoutTool{
- (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:(id<AVPictureInPictureSampleBufferPlaybackDelegate>)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)updateToolboxSpecialEntries{
if([self isOscLayoutToolEnabled]){
if(![toolBoxViewController.specialEntries containsObject:@"widgetLayoutTool"]) [toolBoxViewController.specialEntries insertObject:@"widgetLayoutTool" atIndex:0];
if(![toolBoxViewController.specialEntries containsObject:@"widgetSwitchTool"]) [toolBoxViewController.specialEntries insertObject:@"widgetSwitchTool" atIndex:1];
}
else{
[toolBoxViewController.specialEntries removeObject:@"widgetLayoutTool"];
[toolBoxViewController.specialEntries removeObject:@"widgetSwitchTool"];
}
if(_settings.enablePIP){
if(![toolBoxViewController.specialEntries containsObject:@"enterPip"]) [toolBoxViewController.specialEntries addObject:@"enterPip"];
}
else [toolBoxViewController.specialEntries removeObject:@"enterPip"];
NSLog(@"toolBoxViewController.specialEntries %@", toolBoxViewController.specialEntries);
}
- (void)configOscLayoutTool{
if([self isOscLayoutToolEnabled]){
_oscLayoutTapRecoginizer = [[CustomTapGestureRecognizer alloc] initWithTarget:self action:@selector(openWidgetLayoutTool)];
_oscLayoutTapRecoginizer.numberOfTouchesRequired = _settings.oscLayoutToolFingers.intValue; //tap a predefined number of fingers to open osc layout tool
_oscLayoutTapRecoginizer.tapDownTimeThreshold = 0.2;
@@ -122,15 +242,13 @@
_layoutOnScreenControlsVC.view.backgroundColor = UIColor.clearColor;
_layoutOnScreenControlsVC.modalPresentationStyle = UIModalPresentationOverCurrentContext;
}
else{
[toolBoxViewController.specialEntries removeObject:@"widgetLayoutTool"];
[toolBoxViewController.specialEntries removeObject:@"widgetSwitchTool"];
}
}
- (void)presentCommandManagerViewController{
ToolboxViewController* oldToolboxVC = toolBoxViewController;
toolBoxViewController = [[ToolboxViewController alloc] init];
toolBoxViewController.specialEntryDelegate = self;
toolBoxViewController.specialEntries = oldToolboxVC.specialEntries;
[self configOscLayoutTool];
[self presentViewController:toolBoxViewController animated:YES completion:^{
//[self->toolBoxViewController setupConstraints];
@@ -191,6 +309,7 @@
_settings = [[[DataManager alloc] init] getSettings]; //StreamFrameViewController retrieve the settings here.
overlayLevel = _settings.statsOverlayLevel.intValue;
[self configOscLayoutTool];
[self updateToolboxSpecialEntries];
[self configSwipeGestures];
[self configZoomGestureAndAddStreamView];
[self->_streamView disableOnScreenControls]; //don't know why but this must be called outside the streamview class, just put it here. execute in streamview class cause hang
@@ -224,22 +343,26 @@
[super viewDidAppear:animated];
_deviceWindow = self.view.window;
if (UIScreen.screens.count > 1 && [self isAirPlayEnabled]) {
[self prepExtScreen:UIScreen.screens.lastObject];
if (@available(iOS 13.0, *)) {
UIScreen *currentScreen = self.view.window.windowScene.screen;
if (UIScreen.screens.count > 1 && [self isAirPlayEnabled] && currentScreen == UIScreen.mainScreen) {
[SceneDelegate setExternalDisplayRenderView:self->_streamVideoRenderView];
}
else {
/*
_settings.externalDisplayMode.intValue:
0 - stage manager
1 - airplay
2 - disabled
*/
dispatch_async(dispatch_get_main_queue(), ^{
[self->_streamView insertSubview:self->_streamVideoRenderView atIndex:0];
});
}
} else {
// Fallback on earlier versions
}
else {
/*
_settings.externalDisplayMode.intValue:
0 - stage manager
1 - airplay
2 - disabled
*/
dispatch_async(dispatch_get_main_queue(), ^{
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 +393,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 +423,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 +431,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 +439,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 +461,9 @@
[confirmAction setValue:[NSString stringWithFormat:[LocalizationHelper localizedStringForKey:@"Got it! (%ld)", remainingSeconds], (long)remainingSeconds] forKey:@"title"];
}
});
dispatch_resume(timer);
}
@@ -348,7 +471,7 @@
{
viewJustLoaded = true;
[super viewDidLoad];
[self.navigationController setNavigationBarHidden:YES animated:YES];
[UIApplication sharedApplication].idleTimerDisabled = YES;
@@ -381,25 +504,24 @@
toolBoxViewController = [[ToolboxViewController alloc] init];
toolBoxViewController.specialEntryDelegate = self;
_isRestoringFromPiP = NO;
/*
_settings.externalDisplayMode.intValue:
0 - stage manager
1 - airplay
2 - disabled
*/
if(_settings.externalDisplayMode.intValue == 2) _streamVideoRenderView = _streamView;
else {
//NSLog(@"renderView separated from streamView");
_streamVideoRenderView = (StreamView*)[[UIView alloc] initWithFrame:self.view.frame];
_streamVideoRenderView.bounds = _streamView.bounds;
_streamVideoRenderView.userInteractionEnabled = false; // this will prevent renderView from intrecepting touchEvents which shall be interacting with streamView
}
// A separate render view is always created to support external displays.
_streamVideoRenderView = (StreamView*)[[UIView alloc] initWithFrame:self.view.frame];
_streamVideoRenderView.bounds = _streamView.bounds;
_streamVideoRenderView.userInteractionEnabled = false;
//[_streamView setupStreamView:_controllerSupport interactionDelegate:self config:self.streamConfig];
[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:)];
@@ -516,6 +638,10 @@
[self->_streamView readyToBringUpSoftKeyboardByToolbox];
}
- (void)enterPip{
[self.pipController startPictureInPicture];
}
- (void)oscLayoutClosed{
// Handle the callback
[self->_streamView disableOnScreenControls]; // add this to get realtime back menu working.
@@ -541,7 +667,7 @@
if (parent == nil) {
//NSLog(@"gyro cleanup, count: %ld", _controller.count);
[_controllerSupport cleanup];
[UIApplication sharedApplication].idleTimerDisabled = NO;
[_streamMan stopStream];
if (_inactivityTimer != nil) {
@@ -642,7 +768,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 +785,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 +821,10 @@
}
- (BOOL) isAirPlaying{
return _extWindow != nil && _extWindow.hidden == NO;
if (_settings.externalDisplayMode.intValue == 1 && _streamVideoRenderView) {
return _streamVideoRenderView.hidden;
}
return NO;
}
- (BOOL) isAirPlayEnabled{
@@ -685,45 +834,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,10 +855,16 @@
// This will fire if the user opens control center or gets a low battery message
- (void)applicationWillResignActive:(NSNotification *)notification {
//[self.pipController startPictureInPicture];
//sleep(1);
if (_inactivityTimer != nil) {
[_inactivityTimer invalidate];
_inactivityTimer = nil;
}
#if !TARGET_OS_TV
// Terminate the stream if the app is inactive for 60 seconds
Log(LOG_I, @"Starting inactivity termination timer");
@@ -769,19 +891,27 @@
[_inactivityTimer invalidate];
_inactivityTimer = nil;
}
if (self.pipController && self.pipController.isPictureInPictureActive) {
[self.pipController stopPictureInPicture];
}
//self.pipController = nil;
//[self setupPiPControllerWithRenderer:self->_streamMan.videoRenderer];
//self->_streamMan.videoRenderer.
}
// 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;
NSLog(@"did enter background, %d, %@, %d", _settings.enablePIP, self.pipController, self.pipController.isPictureInPictureActive);
if (_settings.enablePIP && self.pipController && self.pipController.isPictureInPictureActive) {
Log(LOG_I, @"PIP is active, not terminating stream");
return;
}
*/
// [self returnToMainFrame];
#if !TARGET_OS_TV
#endif
}
- (void)expandSettingsView{
@@ -1059,8 +1189,21 @@
}
- (void) videoContentShown {
[_spinner stopAnimating];
[self.view setBackgroundColor:[UIColor blackColor]];
dispatch_async(dispatch_get_main_queue(), ^{
[self->_spinner stopAnimating];
[self.view setBackgroundColor:[UIColor blackColor]];
if (@available(iOS 15.0, *)) {
if (self->_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
@@ -12,6 +12,7 @@ import UIKit
@objc optional func openWidgetLayoutTool()
@objc optional func switchWidgetProfile()
@objc optional func bringUpSoftKeyboard()
@objc optional func enterPip()
}
@@ -27,11 +28,12 @@ import UIKit
private let viewBackgroundColor = UIColor(white: 0.2, alpha: 0.8);
private let highlightColor = UIColor(white: 0.39, alpha: 0.8);
private let titleLabel = UILabel()
@objc public var specialEntries : NSMutableArray = ["widgetSwitchTool", "widgetLayoutTool", "bringUpSoftKeyboard"]
@objc public var specialEntries : NSMutableArray = ["widgetSwitchTool", "widgetLayoutTool", "bringUpSoftKeyboard", "enterPip"]
private let specialEntryAliasDic : [String:String] = [
"widgetSwitchTool":SwiftLocalizationHelper.localizedString(forKey: "[ Switch among on-screen widget profiles ]"),
"widgetLayoutTool":SwiftLocalizationHelper.localizedString(forKey: "[ Open on-screen widget tool ]"),
"bringUpSoftKeyboard":SwiftLocalizationHelper.localizedString(forKey: "[ Bring up soft keyboard ]")
"bringUpSoftKeyboard":SwiftLocalizationHelper.localizedString(forKey: "[ Bring up soft keyboard ]"),
"enterPip":SwiftLocalizationHelper.localizedString(forKey: "[ Enter picture-in-picture mode ]")
]
private var viewPinned: Bool = false
@@ -374,6 +376,8 @@ import UIKit
specialEntryDelegate?.switchWidgetProfile?()
case "bringUpSoftKeyboard":
specialEntryDelegate?.bringUpSoftKeyboard?()
case "enterPip":
specialEntryDelegate?.enterPip?()
default: break
}
}