Merge branch 'Integration' into feat/frame_pacing_metal
# Conflicts: # VoidLink.xcodeproj/project.pbxproj # VoidLink/Base.lproj/iPad.storyboard # VoidLink/Database/DataManager.m # VoidLink/Database/TemporarySettings.m # VoidLink/SceneDelegate.h # VoidLink/Stream/StreamManager.m # VoidLink/Stream/VideoDecoderRenderer.h # VoidLink/Stream/VideoDecoderRenderer.m # VoidLink/ViewControllers/MainFrameViewController.m # VoidLink/ViewControllers/SettingsViewController.m
This commit is contained in:
@@ -75,6 +75,8 @@
|
||||
StreamFrameViewController* streamFrameViewController;
|
||||
id navBarAppearanceStandard;
|
||||
|
||||
NSTimer *_foregroundHostUpdateTimer;
|
||||
|
||||
#if TARGET_OS_TV
|
||||
UITapGestureRecognizer* _menuRecognizer;
|
||||
#endif
|
||||
@@ -1497,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;
|
||||
self.collectionView.allowsMultipleSelection = NO;
|
||||
@@ -1528,7 +1516,6 @@ static NSMutableSet* hostList;
|
||||
|
||||
// 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.
|
||||
@@ -1638,10 +1625,13 @@ static NSMutableSet* hostList;
|
||||
{
|
||||
if (!_background) {
|
||||
// This will kick off box art caching
|
||||
[self updateHosts];
|
||||
|
||||
|
||||
_foregroundHostUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:5 repeats:YES block:^(NSTimer *timer) {
|
||||
[self updateHosts];
|
||||
}];
|
||||
|
||||
// Reset state first so we can rediscover hosts that were deleted before
|
||||
[_discMan resetDiscoveryState];
|
||||
|
||||
[_discMan startDiscovery];
|
||||
|
||||
// This will refresh the applist when a paired host is selected
|
||||
@@ -1743,7 +1733,12 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
|
||||
|
||||
- (void)viewWillDisappear:(BOOL)animated{
|
||||
NSLog(@"willDisappear");
|
||||
[super viewWillDisappear:animated];
|
||||
[_foregroundHostUpdateTimer invalidate];
|
||||
_foregroundHostUpdateTimer = nil;
|
||||
}
|
||||
|
||||
- (void)viewWillAppear:(BOOL)animated
|
||||
{
|
||||
@@ -1862,13 +1857,13 @@ static NSMutableSet* hostList;
|
||||
}
|
||||
|
||||
- (void)updateHosts {
|
||||
Log(LOG_I, @"Updating hosts...");
|
||||
Log(LOG_I, @"Updating hosts %f", CACurrentMediaTime());
|
||||
@synchronized (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) [self.hostCollectionVC addHost:comp];
|
||||
|
||||
if(comp.state == StateOnline || comp.pairState == PairStatePaired || comp.pairState == PairStateUnknown) [self.hostCollectionVC addHost:comp];
|
||||
|
||||
// new host card test
|
||||
// if([comp.name isEqualToString: @"ASRockPC"]) {
|
||||
@@ -1903,6 +1898,9 @@ static NSMutableSet* hostList;
|
||||
|
||||
// Update the title in case we now have a PC
|
||||
[self updateTitle];
|
||||
|
||||
// Reset state first so we can rediscover hosts that were deleted before
|
||||
[_discMan resetDiscoveryState];
|
||||
}
|
||||
|
||||
// This function forces immediate decoding of the UIImage, rather
|
||||
|
||||
@@ -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;
|
||||
@@ -75,6 +76,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;
|
||||
|
||||
@@ -255,7 +255,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;
|
||||
@@ -484,9 +484,10 @@ 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];
|
||||
[self addSetting:self.frameQueueSizeStack ofId:@"frameQueueSizeStack" withInfoTag:NO withDynamicLabel:NO to:videoSection];
|
||||
[self addSetting:self.renderingBackendStack ofId:@"renderingBackendStack" withInfoTag:YES withDynamicLabel:NO to:videoSection];
|
||||
|
||||
|
||||
[videoSection addToParentStack:_parentStack];
|
||||
[videoSection setExpanded:YES];
|
||||
|
||||
@@ -1254,8 +1255,9 @@ BOOL isCustomResolution(CGSize res) {
|
||||
}
|
||||
|
||||
[self.yuv444Switch setOn:currentSettings.enableYUV444];
|
||||
[self.pipSwitch setOn:currentSettings.enablePIP];
|
||||
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
|
||||
|
||||
|
||||
NSInteger renderingBackend = [currentSettings.renderingBackend integerValue];
|
||||
[self.renderingBackendSelector setSelectedSegmentIndex:renderingBackend];
|
||||
|
||||
@@ -1287,7 +1289,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
[self.bitrateSlider addTarget:self action:@selector(bitrateSliderMoved) forControlEvents:UIControlEventValueChanged];
|
||||
[self updateBitrateText];
|
||||
[self updateResolutionDisplayLabel];
|
||||
|
||||
|
||||
[self.frameQueueSizeSlider setMinimumValue:1];
|
||||
[self.frameQueueSizeSlider setMaximumValue:5];
|
||||
[self.frameQueueSizeSlider setValue:_frameQueueSize];
|
||||
@@ -1988,16 +1990,16 @@ BOOL isCustomResolution(CGSize res) {
|
||||
NSInteger onscreenControls = [self.onScreenWidgetSelector selectedSegmentIndex];
|
||||
NSInteger keyboardToggleFingers = self.softKeyboardGestureSelector.selectedSegmentIndex == 3 ? 20 : self.softKeyboardGestureSelector.selectedSegmentIndex+3;
|
||||
NSInteger oscLayoutToolFingers = (uint16_t)self->oswLayoutFingers;
|
||||
|
||||
|
||||
CGFloat slideToSettingsDistance = self.slideToMenuDistanceSlider.value;
|
||||
uint32_t slideToSettingsScreenEdge = [self getScreenEdgeFromSelector];
|
||||
CGFloat pointerVelocityModeDivider = (CGFloat)(uint8_t)self.pointerVelocityModeDividerSlider.value/100;
|
||||
CGFloat touchPointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.touchPointerVelocityFactorSlider.value]/100;
|
||||
CGFloat mousePointerVelocityFactor = (CGFloat)(uint16_t)[self map_velocFactorDisplay_fromSliderValue:self.mousePointerVelocityFactorSlider.value]/100;
|
||||
CGFloat gyroSensitivity = (CGFloat)(uint16_t)self.gyroSensitivitySlider.value/100;
|
||||
|
||||
|
||||
uint16_t touchMoveEventInterval = (uint16_t)self.touchMoveEventIntervalSlider.value;
|
||||
|
||||
|
||||
BOOL reverseMouseWheelDirection = [self.reverseMouseWheelDirectionSelector selectedSegmentIndex] == 1;
|
||||
NSInteger asyncNativeTouchPriority = 1;
|
||||
//BOOL liftStreamViewForKeyboard = [self.liftStreamViewForKeyboardSelector selectedSegmentIndex] == 1;
|
||||
@@ -2010,6 +2012,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;
|
||||
@@ -2047,6 +2050,7 @@ BOOL isCustomResolution(CGSize res) {
|
||||
audioOnPC:audioOnPC
|
||||
preferredCodec:preferredCodec
|
||||
enableYUV444:enableYUV444
|
||||
enablePIP:enablePIP
|
||||
useFramePacing:useFramePacing
|
||||
enableHdr:enableHdr
|
||||
btMouseSupport:btMouseSupport
|
||||
|
||||
@@ -17,18 +17,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;
|
||||
|
||||
@property (nonatomic, strong) MetalViewController *metalViewController;
|
||||
|
||||
@@ -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 "PaddedLabel.h"
|
||||
@@ -75,6 +77,8 @@
|
||||
*/
|
||||
UIWindow *_deviceWindow;
|
||||
dispatch_block_t _delayedRemoveExtScreen;
|
||||
VideoDecoderRenderer *_videoRenderer;
|
||||
BOOL _isRestoringFromPiP;
|
||||
#if !TARGET_OS_TV
|
||||
CustomEdgeSlideGestureRecognizer *_slideToSettingsRecognizer;
|
||||
CustomEdgeSlideGestureRecognizer *_slideToCmdToolRecognizer;
|
||||
@@ -86,12 +90,67 @@
|
||||
UITapGestureRecognizer *_menuDoubleTapGestureRecognizer;
|
||||
UITapGestureRecognizer *_playPauseTapGestureRecognizer;
|
||||
#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+ 可省略
|
||||
@@ -105,10 +164,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;
|
||||
@@ -132,15 +252,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];
|
||||
@@ -200,6 +318,7 @@
|
||||
|
||||
_settings = [[[DataManager alloc] init] getSettings]; //StreamFrameViewController retrieve the settings here.
|
||||
[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
|
||||
@@ -233,22 +352,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
|
||||
@@ -279,17 +402,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
|
||||
}
|
||||
|
||||
@@ -309,7 +432,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);
|
||||
@@ -317,7 +440,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
|
||||
@@ -325,17 +448,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);
|
||||
@@ -347,9 +470,9 @@
|
||||
[confirmAction setValue:[NSString stringWithFormat:[LocalizationHelper localizedStringForKey:@"Got it! (%ld)", remainingSeconds], (long)remainingSeconds] forKey:@"title"];
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
dispatch_resume(timer);
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -357,7 +480,7 @@
|
||||
{
|
||||
viewJustLoaded = true;
|
||||
[super viewDidLoad];
|
||||
|
||||
|
||||
[self.navigationController setNavigationBarHidden:YES animated:YES];
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = YES;
|
||||
@@ -390,25 +513,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:)];
|
||||
@@ -545,6 +667,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.
|
||||
@@ -570,7 +696,7 @@
|
||||
if (parent == nil) {
|
||||
//NSLog(@"gyro cleanup, count: %ld", _controller.count);
|
||||
[_controllerSupport cleanup];
|
||||
|
||||
|
||||
[UIApplication sharedApplication].idleTimerDisabled = NO;
|
||||
[_streamMan stopStream];
|
||||
if (_inactivityTimer != nil) {
|
||||
@@ -666,7 +792,12 @@
|
||||
- (void) returnToMainFrame {
|
||||
// Reset display mode back to default
|
||||
[self updatePreferredDisplayMode:NO];
|
||||
|
||||
[SceneDelegate clearExternalDisplayRenderView];
|
||||
|
||||
if (_settings.enablePIP) {
|
||||
[self cleanupPiPController];
|
||||
}
|
||||
|
||||
[_statsUpdateTimer invalidate];
|
||||
_statsUpdateTimer = nil;
|
||||
|
||||
@@ -678,19 +809,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,7 +845,10 @@
|
||||
}
|
||||
|
||||
- (BOOL) isAirPlaying{
|
||||
return _extWindow != nil && _extWindow.hidden == NO;
|
||||
if (_settings.externalDisplayMode.intValue == 1 && _streamVideoRenderView) {
|
||||
return _streamVideoRenderView.hidden;
|
||||
}
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL) isAirPlayEnabled{
|
||||
@@ -709,45 +858,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;
|
||||
@@ -763,14 +879,20 @@
|
||||
|
||||
// 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");
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:1800
|
||||
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:18000
|
||||
target:self
|
||||
selector:@selector(inactiveTimerExpired:)
|
||||
userInfo:nil
|
||||
@@ -793,19 +915,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{
|
||||
@@ -1083,8 +1213,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) observeFloat:(int)plotId value:(CFTimeInterval)value {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user