From 6a36e734635cb3f031b346290a1e4163f915f745 Mon Sep 17 00:00:00 2001 From: Acaki Date: Sat, 12 Jul 2025 02:47:27 +0800 Subject: [PATCH] fix: new settings --- VoidLink/Base.lproj/iPad.storyboard | 95 +++++++++- .../ViewControllers/SettingsViewController.h | 3 + .../ViewControllers/SettingsViewController.m | 5 + .../StreamFrameViewController.m | 173 +++++++++++++++--- VoidLink/mul.lproj/iPad.xcstrings | 84 +++++++++ 5 files changed, 335 insertions(+), 25 deletions(-) diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index efea9bbd..66a7f6dd 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -1,8 +1,8 @@ - + - + @@ -870,6 +870,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -888,14 +966,19 @@ + + + + + @@ -916,9 +999,12 @@ + + + @@ -1583,6 +1669,8 @@ + + @@ -1595,6 +1683,9 @@ + + + diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index 83eda8c3..f2a2dffa 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -56,6 +56,9 @@ @property (strong, nonatomic) IBOutlet UIStackView *unlockDisplayOrientationStack; @property (strong, nonatomic) IBOutlet UIStackView *externalDisplayModeStack; @property (strong, nonatomic) IBOutlet UIStackView *localMousePointerModeStack; +@property (strong, nonatomic) IBOutlet UIStackView *renderingBackendStack; +@property (strong, nonatomic) IBOutlet UIStackView *frameQueueSizeStack; +@property (strong, nonatomic) IBOutlet UIStackView *performanceGraphStack; @property (strong, nonatomic) IBOutlet UILabel *bitrateLabel; @property (strong, nonatomic) IBOutlet UISlider *bitrateSlider; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index bb1b1626..da5f28b7 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -484,6 +484,9 @@ 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.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]; @@ -556,6 +559,8 @@ BOOL isCustomResolution(CGSize res) { [self addSetting:self.optimizeGamesStack ofId:@"optimizeGamesStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; [self addSetting:self.multiControllerStack ofId:@"multiControllerStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; [self addSetting:self.softKeyboardToolbarStack ofId:@"softKeyboardToolbarStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; + [self addSetting:self.performanceGraphStack ofId:@"performanceGraphStack" withInfoTag:YES withDynamicLabel:NO to:otherSection]; + [otherSection addToParentStack:_parentStack]; [otherSection setExpanded:YES]; diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index e907875e..97542259 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -15,6 +15,10 @@ #import "StreamManager.h" #import "ControllerSupport.h" #import "DataManager.h" +#import "PaddedLabel.h" +#import "ImGuiRenderer.h" +#import "RelativeTouchHandler.h" +#import "MetalVideoRenderer.h" #import "CustomEdgeSlideGestureRecognizer.h" #import "CustomTapGestureRecognizer.h" #import "LocalizationHelper.h" @@ -46,10 +50,10 @@ TemporarySettings *_settings; NSTimer *_inactivityTimer; NSTimer *_statsUpdateTimer; + PaddedLabel *_overlayView; UITapGestureRecognizer *_menuTapGestureRecognizer; UITapGestureRecognizer *_menuDoubleTapGestureRecognizer; UITapGestureRecognizer *_playPauseTapGestureRecognizer; - UITextView *_overlayView; UILabel *_stageLabel; UILabel *_tipLabel; UIActivityIndicatorView *_spinner; @@ -58,6 +62,9 @@ BOOL _userIsInteracting; bool viewJustLoaded; CGSize _keyboardSize; + PlotMetrics _decodeMetrics; + PlotMetrics _frameDropMetrics; + PlotMetrics _frameQueueMetrics; UIWindow *_extWindow; UIView *_streamVideoRenderView; /* @@ -74,6 +81,13 @@ CustomTapGestureRecognizer *_oscLayoutTapRecoginizer; LayoutOnScreenControlsViewController *_layoutOnScreenControlsVC; ToolboxViewController* toolBoxViewController; + UISwipeGestureRecognizer *_topSwipeRecognizer; + UISwipeGestureRecognizer *_topSwipeUpRecognizer; +#else + UITapGestureRecognizer *_menuTapGestureRecognizer; + UITapGestureRecognizer *_menuDoubleTapGestureRecognizer; + UITapGestureRecognizer *_playPauseTapGestureRecognizer; + UITapGestureRecognizer *_remoteDoubleSelectRecognizer; #endif } @@ -292,6 +306,14 @@ Log(LOG_I, @"Play/Pause button pressed -- backing out of stream"); [self returnToMainFrame]; } +- (void)remoteSelectButtonDoublePressed:(id)sender { + Log(LOG_I, @"Select button double-tapped -- toggling stats"); + if (!self->_statsUpdateTimer) { + [self showStats]; + } else { + [self hideStats]; + } +} #endif - (void)popFirstLaunchTip { @@ -399,7 +421,7 @@ if([self isFirstLaunch]) [self popFirstLaunchTip]; #if TARGET_OS_TV - if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer) { + if (!_menuTapGestureRecognizer || !_menuDoubleTapGestureRecognizer || !_playPauseTapGestureRecognizer || !_remoteDoubleSelectRecognizer) { _menuTapGestureRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(controllerPauseButtonPressed:)]; _menuTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)]; @@ -410,15 +432,37 @@ _menuDoubleTapGestureRecognizer.numberOfTapsRequired = 2; [_menuTapGestureRecognizer requireGestureRecognizerToFail:_menuDoubleTapGestureRecognizer]; _menuDoubleTapGestureRecognizer.allowedPressTypes = @[@(UIPressTypeMenu)]; + + _remoteDoubleSelectRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(remoteSelectButtonDoublePressed:)]; + _remoteDoubleSelectRecognizer.numberOfTapsRequired = 2; + _remoteDoubleSelectRecognizer.allowedPressTypes = @[@(UIPressTypeSelect)]; } [self.view addGestureRecognizer:_menuTapGestureRecognizer]; [self.view addGestureRecognizer:_menuDoubleTapGestureRecognizer]; [self.view addGestureRecognizer:_playPauseTapGestureRecognizer]; + [self.view addGestureRecognizer:_remoteDoubleSelectRecognizer]; #else - //[self configSwipeGestures]; // swipe & exit gesture configured here - //[self configOscLayoutTool]; //_oscLayoutTapRecoginizer will be added or removed to the view here + _topSwipeRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(topSwiped)]; + _topSwipeRecognizer.direction = UISwipeGestureRecognizerDirectionDown; + _topSwipeRecognizer.numberOfTouchesRequired = 2; + _topSwipeRecognizer.enabled = TRUE; + [self.view addGestureRecognizer:_topSwipeRecognizer]; + + _topSwipeUpRecognizer = [[UISwipeGestureRecognizer alloc] initWithTarget:self action:@selector(topSwipedUp)]; + _topSwipeUpRecognizer.direction = UISwipeGestureRecognizerDirectionUp; + _topSwipeUpRecognizer.numberOfTouchesRequired = 2; + _topSwipeUpRecognizer.enabled = FALSE; + // This is added to the _overlayView when displayed +#endif + + _streamView = [[StreamView alloc] initWithFrame:self.view.frame]; +#if TARGET_OS_TV + // we need to tell the other remote handler in RelativeTouchHandler to wait for our double-select + RelativeTouchHandler *touchHandler = (RelativeTouchHandler *)[_streamView touchHandler]; + UIGestureRecognizer *remotePressRecognizer = [touchHandler remotePressRecognizer]; + [remotePressRecognizer requireGestureRecognizerToFail:_remoteDoubleSelectRecognizer]; #endif _tipLabel = [[UILabel alloc] init]; @@ -479,6 +523,26 @@ [self.view addSubview:_stageLabel]; [self.view addSubview:_spinner]; [self.view addSubview:_tipLabel]; + + if ([_settings.renderingBackend intValue] == RENDER_METAL) { + // Metal view for video + // TODO: refactor the way things access observeFloat for stats + self.metalViewController = [[MetalViewController alloc] initWithFrame:self.view.bounds + framerate:[self->_settings.framerate floatValue] + enableHdr:self->_settings.enableHdr + metricsHandler:self.imguiView.metricsHandler]; + [self.view addSubview:self.metalViewController.view]; + [self.view bringSubviewToFront:self.metalViewController.view]; + } + + // Make a MetalKit view for ImGui + self.imguiView = [[ImGuiRenderer alloc] initWithFrame:self.view.bounds + streamFps:[_settings.framerate intValue] + enableGraphs:_settings.enableGraphs + graphOpacity:[_settings.graphOpacity intValue]]; + [self.view addSubview:self.imguiView.mtkView]; + [self.view bringSubviewToFront:self.imguiView.mtkView]; + } - (void)keyboardWillShow:(NSNotification *)notification{ @@ -589,13 +653,12 @@ - (void)updateOverlayText:(NSString*)text { if (_overlayView == nil) { - _overlayView = [[UITextView alloc] init]; -#if !TARGET_OS_TV - [_overlayView setEditable:NO]; -#endif - [_overlayView setUserInteractionEnabled:NO]; - [_overlayView setSelectable:NO]; - [_overlayView setScrollEnabled:NO]; + _overlayView = [[PaddedLabel alloc] initWithFrame:CGRectZero]; + [_overlayView setTextInsets:UIEdgeInsetsMake(10, 15, 10, 15)]; + [_overlayView setUserInteractionEnabled:YES]; + [_overlayView setNumberOfLines:100]; + [_overlayView.layer setCornerRadius:12]; + [_overlayView.layer setMasksToBounds:YES]; // HACK: If not using stats overlay, center the text if (_statsUpdateTimer == nil) { @@ -605,17 +668,15 @@ [_overlayView setTextColor:[UIColor lightGrayColor]]; [_overlayView setBackgroundColor:[UIColor blackColor]]; #if TARGET_OS_TV - [_overlayView setFont:[UIFont systemFontOfSize:24]]; + [_overlayView setFont:[UIFont systemFontOfSize:24 weight:UIFontWeightMedium]]; #else - if (@available(iOS 13.0, *)) { - [_overlayView setFont:[UIFont monospacedSystemFontOfSize:12 weight:UIFontWeightRegular]]; - } else { - [_overlayView setFont:[UIFont systemFontOfSize:12]];// Fallback on earlier versions - } - //[_overlayView setFont:[UIFont fontWithName:@"Menlo" size:12]]; + [_overlayView setFont:[UIFont systemFontOfSize:12 weight:UIFontWeightMedium]]; + _topSwipeUpRecognizer.enabled = TRUE; + [_overlayView addGestureRecognizer:_topSwipeUpRecognizer]; #endif - [_overlayView setAlpha:0.5]; + int opacity = MAX([_settings.graphOpacity intValue], 60); + [_overlayView setAlpha:(float)opacity / 100.0]; [self.view addSubview:_overlayView]; } @@ -629,7 +690,7 @@ _overlayView.frame.size.height)]; [_overlayView setText:text]; [_overlayView sizeToFit]; - [_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, _overlayView.frame.size.height / 2)]; + [_overlayView setCenter:CGPointMake(self.view.frame.size.width / 2, (12 + (_overlayView.frame.size.height / 2)))]; [_overlayView setHidden:NO]; } else { @@ -643,7 +704,12 @@ [_statsUpdateTimer invalidate]; _statsUpdateTimer = nil; - + +#if !TARGET_OS_TV + _topSwipeRecognizer.enabled = FALSE; + _topSwipeUpRecognizer.enabled = FALSE; +#endif + [self.navigationController popToRootViewControllerAnimated:YES]; _extWindow = nil; @@ -802,6 +868,58 @@ [self returnToMainFrame]; } +- (void)topSwiped { + Log(LOG_I, @"User swiped/cicked down for stats"); + [self showStats]; + +#if !TARGET_OS_TV + _topSwipeRecognizer.enabled = FALSE; + _topSwipeUpRecognizer.enabled = TRUE; +#endif +} + +- (void)showStats { + if (self->_statsUpdateTimer == nil) { + self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f + target:self + selector:@selector(updateStatsOverlay) + userInfo:nil + repeats:YES]; + [self->_statsUpdateTimer fire]; + + if (_settings.enableGraphs) { + [self.imguiView start]; + [self.imguiView show]; + } + } +} + +- (void)topSwipedUp { + Log(LOG_I, @"User swiped up to hide stats"); + [self hideStats]; + +#if !TARGET_OS_TV + _topSwipeRecognizer.enabled = TRUE; + _topSwipeUpRecognizer.enabled = FALSE; +#endif +} + +- (void)hideStats { + if (self->_statsUpdateTimer != nil) { + [_statsUpdateTimer invalidate]; + _statsUpdateTimer = nil; + } + + if (_overlayView != nil) { + [_overlayView setHidden:YES]; + } + + if (_settings.enableGraphs) { + [self.imguiView hide]; + [self.imguiView stop]; + } +} + - (void) connectionStarted { Log(LOG_I, @"Connection started"); dispatch_async(dispatch_get_main_queue(), ^{ @@ -816,6 +934,7 @@ [self->_controllerSupport connectionEstablished]; if (self->_settings.statsOverlayEnabled) { + [self topSwiped]; self->_statsUpdateTimer = [NSTimer scheduledTimerWithTimeInterval:1.0f target:self selector:@selector(updateStatsOverlay) @@ -1025,7 +1144,7 @@ if (@available(tvOS 11.2, *)) { UIWindow* window = [[[UIApplication sharedApplication] delegate] window]; AVDisplayManager* displayManager = [window avDisplayManager]; - + // This logic comes from Kodi and MrMC if (streamActive) { int dynamicRange; @@ -1036,7 +1155,7 @@ else { dynamicRange = 0; // SDR } - + AVDisplayCriteria* displayCriteria = [[AVDisplayCriteria alloc] initWithRefreshRate:[_settings.framerate floatValue] videoDynamicRange:dynamicRange]; displayManager.preferredDisplayCriteria = displayCriteria; @@ -1061,6 +1180,14 @@ [self.view setBackgroundColor:[UIColor blackColor]]; } +- (void) observeFloat:(int)plotId value:(CFTimeInterval)value { + [self.imguiView observeFloat:plotId value:value]; +} + +- (void) observeFloatReturnMetrics:(int)plotId value:(CFTimeInterval)value plotMetrics:(PlotMetrics *)plotMetrics { + return [self.imguiView observeFloatReturnMetrics:plotId value:value plotMetrics:plotMetrics]; +} + - (void)didReceiveMemoryWarning { [super didReceiveMemoryWarning]; diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index bd96247e..dd9be24e 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -349,6 +349,30 @@ } } }, + "53H-ST-OtY.segmentTitles[0]" : { + "comment" : "Class = \"UISegmentedControl\"; 53H-ST-OtY.segmentTitles[0] = \"Metal\"; ObjectID = \"53H-ST-OtY\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Metal" + } + } + } + }, + "53H-ST-OtY.segmentTitles[1]" : { + "comment" : "Class = \"UISegmentedControl\"; 53H-ST-OtY.segmentTitles[1] = \"AVSampleBuffer\"; ObjectID = \"53H-ST-OtY\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "AVSampleBuffer" + } + } + } + }, "aa5-3y-I4d.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; aa5-3y-I4d.segmentTitles[0] = \"Off\"; ObjectID = \"aa5-3y-I4d\";", "extractionState" : "extracted_with_value", @@ -853,6 +877,30 @@ } } }, + "ccm-n2-PXi.text" : { + "comment" : "Class = \"UILabel\"; text = \"Performance Graphs - Opacity: 50%\"; ObjectID = \"ccm-n2-PXi\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Performance Graphs - Opacity: 50%" + } + } + } + }, + "Ce3-FM-vIh.text" : { + "comment" : "Class = \"UILabel\"; text = \"Frames to Buffer\"; ObjectID = \"Ce3-FM-vIh\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Frames to Buffer" + } + } + } + }, "ckc-Dm-8ex.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; ckc-Dm-8ex.segmentTitles[0] = \"360p\"; ObjectID = \"ckc-Dm-8ex\";", "extractionState" : "extracted_with_value", @@ -1081,6 +1129,18 @@ } } }, + "dK2-1g-PUQ.text" : { + "comment" : "Class = \"UILabel\"; text = \"Displays frametime and buffer health metrics. Adjust the opacity if they obscure parts of the screen. Swipe up/down with two fingers to show or hide all stats.\"; ObjectID = \"dK2-1g-PUQ\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Displays frametime and buffer health metrics. Adjust the opacity if they obscure parts of the screen. Swipe up/down with two fingers to show or hide all stats." + } + } + } + }, "Dll-7x-18l.text" : { "comment" : "Class = \"UILabel\"; text = \"Swap A/B and X/Y Buttons\"; ObjectID = \"Dll-7x-18l\";", "extractionState" : "extracted_with_value", @@ -2899,6 +2959,18 @@ } } }, + "v8l-hf-kiR.text" : { + "comment" : "Class = \"UILabel\"; text = \"Rendering Backend\"; ObjectID = \"v8l-hf-kiR\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "Rendering Backend" + } + } + } + }, "vJs-pg-hHe.text" : { "comment" : "Class = \"UILabel\"; text = \"test\"; ObjectID = \"vJs-pg-hHe\";", "extractionState" : "extracted_with_value", @@ -3001,6 +3073,18 @@ } } }, + "Ylu-hT-Cuz.text" : { + "comment" : "Class = \"UILabel\"; text = \"The recommended value is 2. 1 frame provides the lowest latency, but may require an Ethernet connection.\"; ObjectID = \"Ylu-hT-Cuz\";", + "extractionState" : "extracted_with_value", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "new", + "value" : "The recommended value is 2. 1 frame provides the lowest latency, but may require an Ethernet connection." + } + } + } + }, "yY7-Sf-Zes.segmentTitles[0]" : { "comment" : "Class = \"UISegmentedControl\"; yY7-Sf-Zes.segmentTitles[0] = \"Slide From Left Edge\"; ObjectID = \"yY7-Sf-Zes\";", "extractionState" : "extracted_with_value",