refactor: adopt UIScene lifecycle

feat: add initial support for pip

feat: add setting toggle for pip

fix: let os handle invokation of pip which is more reliable

fix: UIScene compatibility with airplay

fix: black screen on external display when on app view and streaming

fix: incorrect window size when launched directly on external display

fix: wrong resolution shown in settings when in stream session

Enable gamemode flag for iOS18+

Explicitly enters pip mode when in background
This commit is contained in:
Acaki committed 2025-07-11 01:34:53 +08:00
1 parent 51e915b598
commit b5ce7a8ff7
23 files changed
+416 -80

No files matched your search

+19 -1
View File
@@ -570,7 +570,7 @@ ICAgICAgICAgICAgIA
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl> </segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Local Mouse Pointer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VGn-Oc-mKc"> <label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Local Mouse Pointer" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="VGn-Oc-mKc">
<rect key="frame" x="22" y="2007" width="165" height="21"/> <rect key="frame" x="20" y="2008" width="165" height="21"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/> <autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/> <fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
@@ -635,6 +635,23 @@ ICAgICAgICAgICAgIA
<action selector="backToStreamingButtonTapped:" destination="rYd-e6-cQU" eventType="touchUpInside" id="9P0-Yw-T4W"/> <action selector="backToStreamingButtonTapped:" destination="rYd-e6-cQU" eventType="touchUpInside" id="9P0-Yw-T4W"/>
</connections> </connections>
</button> </button>
<segmentedControl opaque="NO" contentMode="scaleToFill" fixedFrame="YES" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="dcP-yB-6Un" userLabel="PiP Selector">
<rect key="frame" x="18" y="2102" width="459" height="28"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<segments>
<segment title="No"/>
<segment title="Yes"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</segmentedControl>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" fixedFrame="YES" text="Enable PiP" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="ELc-8r-KSB">
<rect key="frame" x="20" y="2075" width="86" height="19"/>
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="17"/>
<color key="textColor" red="0.93902439019999995" green="0.9625305918" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews> </subviews>
<color key="backgroundColor" red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/> <color key="backgroundColor" red="0.12156862745098039" green="0.12941176470588237" blue="0.14117647058823529" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view> </view>
@@ -664,6 +681,7 @@ ICAgICAgICAgICAgIA
<outlet property="optimizeSettingsSelector" destination="Gob-Lu-b1y" id="9Wl-yp-0Ou"/> <outlet property="optimizeSettingsSelector" destination="Gob-Lu-b1y" id="9Wl-yp-0Ou"/>
<outlet property="oscTapExlusionAreaSizeLabel" destination="Dfl-gL-00M" id="Jbg-64-P3M"/> <outlet property="oscTapExlusionAreaSizeLabel" destination="Dfl-gL-00M" id="Jbg-64-P3M"/>
<outlet property="oscTapExlusionAreaSizeSlider" destination="TSt-Hj-uTR" id="h9l-ap-AVd"/> <outlet property="oscTapExlusionAreaSizeSlider" destination="TSt-Hj-uTR" id="h9l-ap-AVd"/>
<outlet property="pipSelector" destination="dcP-yB-6Un" id="rrB-dR-2Bq"/>
<outlet property="pointerVelocityModeDividerSlider" destination="xet-C9-l5y" id="GnQ-xU-e0N"/> <outlet property="pointerVelocityModeDividerSlider" destination="xet-C9-l5y" id="GnQ-xU-e0N"/>
<outlet property="pointerVelocityModeDividerUILabel" destination="VlN-ZW-1gm" id="vbe-QK-hZb"/> <outlet property="pointerVelocityModeDividerUILabel" destination="VlN-ZW-1gm" id="vbe-QK-hZb"/>
<outlet property="resolutionDisplayView" destination="Dej-Bj-F6i" id="bi4-N6-snx"/> <outlet property="resolutionDisplayView" destination="Dej-Bj-F6i" id="bi4-N6-snx"/>
+12
View File
@@ -0,0 +1,12 @@
#import <UIKit/UIKit.h>
@interface SceneDelegate : UIResponder <UIWindowSceneDelegate>
@property (strong, nonatomic) UIWindow * window;
+ (void)setExternalDisplayRenderView:(UIView *)renderView;
+ (void)clearExternalDisplayRenderView;
- (void)updatePreferredDisplayMode:(BOOL)streamActive withRenderView:(UIView *)renderView;
@end
+91
View File
@@ -0,0 +1,91 @@
#import "SceneDelegate.h"
#import "StreamFrameViewController.h"
@implementation SceneDelegate
static UIView *_sharedStreamVideoRenderView = nil;
static UIWindow *_externalSceneWindow = nil;
- (void)scene:(UIScene *)scene willConnectToSession:(UISceneSession *)session options:(UISceneConnectionOptions *)connectionOptions {
if (![scene isKindOfClass:[UIWindowScene class]]) {
return;
}
UIWindowScene *windowScene = (UIWindowScene *)scene;
if ([session.role isEqualToString:UIWindowSceneSessionRoleApplication]) {
self.window = [[UIWindow alloc] initWithWindowScene:windowScene];
NSString *storyboardName;
if (UIDevice.currentDevice.userInterfaceIdiom == UIUserInterfaceIdiomPad) {
storyboardName = @"iPad";
} else {
storyboardName = @"iPhone";
}
UIStoryboard *storyboard = [UIStoryboard storyboardWithName:storyboardName bundle:nil];
UIViewController *initialViewController = [storyboard instantiateInitialViewController];
self.window.rootViewController = initialViewController;
[self.window makeKeyAndVisible];
Log(LOG_I, @"SceneDelegate: Main app scene connected.");
} else if ([session.role isEqualToString:UIWindowSceneSessionRoleExternalDisplay]) {
Log(LOG_I, @"SceneDelegate: External display scene connecting for screen: %@", ((UIWindowScene *)scene).screen.description);
UIWindowScene *windowScene = (UIWindowScene *)scene;
_externalSceneWindow = [[UIWindow alloc] initWithWindowScene:windowScene];
UIViewController *externalVC = [[UIViewController alloc] init];
externalVC.view.backgroundColor = [UIColor blackColor]; // Set a default background
_externalSceneWindow.rootViewController = externalVC;
if (_sharedStreamVideoRenderView) {
_sharedStreamVideoRenderView.frame = _externalSceneWindow.bounds;
[_externalSceneWindow.rootViewController.view addSubview:_sharedStreamVideoRenderView];
Log(LOG_I, @"SceneDelegate: External display scene connected.");
}
}
}
// Method for StreamFrameViewController to provide its render view
+ (void)setExternalDisplayRenderView:(UIView *)renderView {
_sharedStreamVideoRenderView = renderView;
if (_externalSceneWindow && _externalSceneWindow.rootViewController && _sharedStreamVideoRenderView) {
dispatch_async(dispatch_get_main_queue(), ^{
// Ensure it's removed from any previous parent (should have been done by StreamFrameVC)
[_sharedStreamVideoRenderView removeFromSuperview];
_sharedStreamVideoRenderView.frame = _externalSceneWindow.bounds; // Set frame for external window
[_externalSceneWindow.rootViewController.view addSubview:_sharedStreamVideoRenderView];
_externalSceneWindow.hidden = NO;
Log(LOG_I, @"SceneDelegate: Added render view to external window's root view.");
});
} else {
Log(LOG_E, @"SceneDelegate: External display window or root view controller not available.");
}
}
+ (void)clearExternalDisplayRenderView {
if (_sharedStreamVideoRenderView) {
dispatch_async(dispatch_get_main_queue(), ^{
[_sharedStreamVideoRenderView removeFromSuperview];
Log(LOG_I, @"SceneDelegate: Removed render view from external display.");
});
}
_sharedStreamVideoRenderView = nil;
}
- (void)sceneDidDisconnect:(UIScene *)scene {
Log(LOG_I, @"SceneDelegate: Scene disconnected: %@, role: %@", scene.title, scene.session.role);
if ([scene.session.role isEqualToString:UIWindowSceneSessionRoleExternalDisplay]) {
if ([scene isKindOfClass:[UIWindowScene class]]) {
UIWindowScene *windowScene = (UIWindowScene *)scene;
if (_externalSceneWindow == windowScene.windows.firstObject) { // Compare with the window from the disconnecting scene
[SceneDelegate clearExternalDisplayRenderView]; // Clears the shared view
_externalSceneWindow = nil;
Log(LOG_I, @"SceneDelegate: External display scene fully disconnected and cleaned up.");
} else {
Log(LOG_W, @"SceneDelegate: Disconnecting scene is not the one holding our _externalSceneWindow.");
}
} else {
Log(LOG_W, @"SceneDelegate: Disconnecting scene is not a UIWindowScene.");
}
}
}
@end
@@ -30,6 +30,7 @@
@property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *audioOnPCSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *codecSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector; @property (strong, nonatomic) IBOutlet UISegmentedControl *yuv444Selector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *pipSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *hdrSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *framePacingSelector;
@property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector; @property (strong, nonatomic) IBOutlet UISegmentedControl *btMouseSelector;
@@ -228,7 +228,7 @@ BOOL isCustomResolution(CGSize res) {
} }
- (void)updateResolutionTable{ - (void)updateResolutionTable{
UIWindow *window = UIApplication.sharedApplication.windows.firstObject; UIWindow *window = self.view.window;
CGFloat screenScale = window.screen.scale; CGFloat screenScale = window.screen.scale;
CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale; CGFloat safeAreaWidth = (window.frame.size.width - window.safeAreaInsets.left - window.safeAreaInsets.right) * screenScale;
CGFloat appWindowWidth = window.frame.size.width * screenScale; CGFloat appWindowWidth = window.frame.size.width * screenScale;
@@ -465,6 +465,7 @@ BOOL isCustomResolution(CGSize res) {
} }
[self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0]; [self.yuv444Selector setSelectedSegmentIndex:currentSettings.enableYUV444 ? 1 : 0];
[self.pipSelector setSelectedSegmentIndex:currentSettings.enablePIP ? 1 : 0];
[self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue]; [self.statsOverlaySelector setSelectedSegmentIndex:currentSettings.statsOverlayLevel.intValue];
[self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0]; [self.btMouseSelector setSelectedSegmentIndex:currentSettings.btMouseSupport ? 1 : 0];
[self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0]; [self.optimizeSettingsSelector setSelectedSegmentIndex:currentSettings.optimizeGames ? 1 : 0];
@@ -1079,6 +1080,7 @@ BOOL isCustomResolution(CGSize res) {
BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1; BOOL audioOnPC = [self.audioOnPCSelector selectedSegmentIndex] == 1;
uint32_t preferredCodec = [self getChosenCodecPreference]; uint32_t preferredCodec = [self getChosenCodecPreference];
BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1; BOOL enableYUV444 = [self.yuv444Selector selectedSegmentIndex] == 1;
BOOL enablePIP = [self.pipSelector selectedSegmentIndex] == 1;
BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1; BOOL btMouseSupport = [self.btMouseSelector selectedSegmentIndex] == 1;
BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1; BOOL useFramePacing = [self.framePacingSelector selectedSegmentIndex] == 1;
// BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1; // BOOL absoluteTouchMode = [self.touchModeSelector selectedSegmentIndex] == 1;
@@ -1116,6 +1118,7 @@ BOOL isCustomResolution(CGSize res) {
audioOnPC:audioOnPC audioOnPC:audioOnPC
preferredCodec:preferredCodec preferredCodec:preferredCodec
enableYUV444:enableYUV444 enableYUV444:enableYUV444
enablePIP:enablePIP
useFramePacing:useFramePacing useFramePacing:useFramePacing
enableHdr:enableHdr enableHdr:enableHdr
btMouseSupport:btMouseSupport btMouseSupport:btMouseSupport
+1 -1
View File
@@ -10,7 +10,6 @@
@interface AppDelegate : UIResponder <UIApplicationDelegate> @interface AppDelegate : UIResponder <UIApplicationDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) NSString *pcUuidToLoad; @property (strong, nonatomic) NSString *pcUuidToLoad;
@property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL); @property (strong, nonatomic) void (^shortcutCompletionHandler)(BOOL);
@@ -21,5 +20,6 @@
- (void)saveContext; - (void)saveContext;
- (NSURL *)applicationDocumentsDirectory; - (NSURL *)applicationDocumentsDirectory;
- (NSURL*) getStoreURL; - (NSURL*) getStoreURL;
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0));
@end @end
+9
View File
@@ -27,6 +27,15 @@ static NSString* DB_NAME = @"Moonlight_tvOS.bin";
static NSString* DB_NAME = @"Limelight_iOS.sqlite"; static NSString* DB_NAME = @"Limelight_iOS.sqlite";
#endif #endif
#pragma mark - UISceneSession lifecycle
- (UISceneConfiguration *)application:(UIApplication *)application configurationForConnectingSceneSession:(UISceneSession *)connectingSceneSession options:(UISceneConnectionOptions *)options API_AVAILABLE(ios(13.0)){
return [[UISceneConfiguration alloc] initWithName:@"Default Configuration" sessionRole:connectingSceneSession.role];
}
- (void)application:(UIApplication *)application didDiscardSceneSessions:(NSSet<UISceneSession *> *)sceneSessions API_AVAILABLE(ios(13.0)){
}
#if !TARGET_OS_TV #if !TARGET_OS_TV
+1
View File
@@ -75,6 +75,7 @@ typedef NS_ENUM(NSInteger, SettingsMenuMode) {
audioOnPC:(BOOL)audioOnPC audioOnPC:(BOOL)audioOnPC
preferredCodec:(uint32_t)preferredCodec preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444 enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
useFramePacing:(BOOL)useFramePacing useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport btMouseSupport:(BOOL)btMouseSupport
+2
View File
@@ -81,6 +81,7 @@
audioOnPC:(BOOL)audioOnPC audioOnPC:(BOOL)audioOnPC
preferredCodec:(uint32_t)preferredCodec preferredCodec:(uint32_t)preferredCodec
enableYUV444:(BOOL)enableYUV444 enableYUV444:(BOOL)enableYUV444
enablePIP:(BOOL)enablePIP
useFramePacing:(BOOL)useFramePacing useFramePacing:(BOOL)useFramePacing
enableHdr:(BOOL)enableHdr enableHdr:(BOOL)enableHdr
btMouseSupport:(BOOL)btMouseSupport btMouseSupport:(BOOL)btMouseSupport
@@ -121,6 +122,7 @@
settingsToSave.playAudioOnPC = audioOnPC; settingsToSave.playAudioOnPC = audioOnPC;
settingsToSave.preferredCodec = preferredCodec; settingsToSave.preferredCodec = preferredCodec;
settingsToSave.enableYUV444 = enableYUV444; settingsToSave.enableYUV444 = enableYUV444;
settingsToSave.enablePIP = enablePIP;
settingsToSave.useFramePacing = useFramePacing; settingsToSave.useFramePacing = useFramePacing;
settingsToSave.enableHdr = enableHdr; settingsToSave.enableHdr = enableHdr;
settingsToSave.btMouseSupport = btMouseSupport; settingsToSave.btMouseSupport = btMouseSupport;
+1
View File
@@ -46,6 +46,7 @@
CODEC_PREF_AV1, CODEC_PREF_AV1,
} preferredCodec; } preferredCodec;
@property (nonatomic) BOOL enableYUV444; @property (nonatomic) BOOL enableYUV444;
@property (nonatomic) BOOL enablePIP;
@property (nonatomic) BOOL reverseMouseWheelDirection; @property (nonatomic) BOOL reverseMouseWheelDirection;
@property (nonatomic, retain) NSNumber * asyncNativeTouchPriority; @property (nonatomic, retain) NSNumber * asyncNativeTouchPriority;
@property (nonatomic) BOOL useFramePacing; @property (nonatomic) BOOL useFramePacing;
+2
View File
@@ -41,6 +41,7 @@
assert([self.audioConfig intValue] != 0); assert([self.audioConfig intValue] != 0);
self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"]; self.preferredCodec = (typeof(self.preferredCodec))[[NSUserDefaults standardUserDefaults] integerForKey:@"preferredCodec"];
self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"]; self.enableYUV444 = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableYUV444"];
self.enablePIP = [[NSUserDefaults standardUserDefaults] boolForKey:@"enablePIP"];
self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0; self.useFramePacing = [[NSUserDefaults standardUserDefaults] integerForKey:@"useFramePacing"] != 0;
self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"]; self.playAudioOnPC = [[NSUserDefaults standardUserDefaults] boolForKey:@"audioOnPC"];
self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"]; self.enableHdr = [[NSUserDefaults standardUserDefaults] boolForKey:@"enableHdr"];
@@ -82,6 +83,7 @@
self.audioConfig = settings.audioConfig; self.audioConfig = settings.audioConfig;
self.preferredCodec = settings.preferredCodec; self.preferredCodec = settings.preferredCodec;
self.enableYUV444 = settings.enableYUV444; self.enableYUV444 = settings.enableYUV444;
self.enablePIP = settings.enablePIP;
self.useFramePacing = settings.useFramePacing; self.useFramePacing = settings.useFramePacing;
self.playAudioOnPC = settings.playAudioOnPC; self.playAudioOnPC = settings.playAudioOnPC;
self.enableHdr = settings.enableHdr; self.enableHdr = settings.enableHdr;
+28 -1
View File
@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>GCSupportsGameMode</key>
<true/>
<key>CADisableMinimumFrameDuration</key> <key>CADisableMinimumFrameDuration</key>
<true/> <true/>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
@@ -66,11 +68,36 @@
<string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string> <string>Moonlight uses the device Gyro and Acceleration data to emulate motion controls when streaming.</string>
<key>UIApplicationSceneManifest</key> <key>UIApplicationSceneManifest</key>
<dict> <dict>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleExternalDisplay</key>
<array>
<dict>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
<key>UISceneConfigurationName</key>
<string>External Display Configuration</string>
</dict>
</array>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>SceneDelegate</string>
</dict>
</array>
</dict>
<key>UIApplicationSupportsMultipleScenes</key> <key>UIApplicationSupportsMultipleScenes</key>
<false/> <true/>
</dict> </dict>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true/> <true/>
<key>UIBackgroundModes</key>
<array>
<string>audio</string>
</array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>Launch Screen</string> <string>Launch Screen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -26,6 +26,7 @@
<attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/> <attribute name="bitrate" attributeType="Integer 32" defaultValueString="10000" usesScalarValueType="NO" syncable="YES"/>
<attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="btMouseSupport" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="enableHdr" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enablePIP" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/> <attribute name="enableYUV444" attributeType="Boolean" defaultValueString="NO" usesScalarValueType="YES" syncable="YES"/>
<attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/> <attribute name="externalDisplayMode" optional="YES" attributeType="Integer 16" defaultValueString="1" usesScalarValueType="NO" syncable="YES"/>
<attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/> <attribute name="framerate" attributeType="Integer 32" defaultValueString="60" usesScalarValueType="NO" syncable="YES"/>
+1
View File
@@ -20,6 +20,7 @@
@property NSString* rtspSessionUrl; @property NSString* rtspSessionUrl;
@property int serverCodecModeSupport; @property int serverCodecModeSupport;
@property BOOL enableYUV444; @property BOOL enableYUV444;
@property BOOL enablePIP;
@property int width; @property int width;
@property int height; @property int height;
@property int frameRate; @property int frameRate;
+1 -1
View File
@@ -9,5 +9,5 @@
#import "StreamConfiguration.h" #import "StreamConfiguration.h"
@implementation StreamConfiguration @implementation StreamConfiguration
@synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, gyroMode, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444; @synthesize host, httpsPort, appID, width, height, frameRate, bitRate, riKeyId, riKey, gamepadMask, appName, optimizeGameSettings, playAudioOnPC, swapABXYButtons, gyroMode, audioConfiguration, supportedVideoFormats, multiController, serverCert, rtspSessionUrl, serverCodecModeSupport, enableYUV444, enablePIP;
@end @end
+2
View File
@@ -14,6 +14,8 @@
@interface StreamManager : NSOperation @interface StreamManager : NSOperation
@property (nonatomic, strong, readonly) VideoDecoderRenderer *videoRenderer;
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback; - (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callback;
- (void) stopStream; - (void) stopStream;
+7 -2
View File
@@ -29,6 +29,7 @@
UIView* _renderView; UIView* _renderView;
id<ConnectionCallbacks> _callbacks; id<ConnectionCallbacks> _callbacks;
Connection* _connection; Connection* _connection;
VideoDecoderRenderer* _videoRenderer;
} }
- (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks { - (id) initWithConfig:(StreamConfiguration*)config renderView:(UIView*)view connectionCallbacks:(id<ConnectionCallbacks>)callbacks {
@@ -103,8 +104,12 @@
// Initializing the renderer must be done on the main thread // Initializing the renderer must be done on the main thread
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
VideoDecoderRenderer* renderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView callbacks:self->_callbacks streamAspectRatio:(float)self->_config.width / (float)self->_config.height useFramePacing:self->_config.useFramePacing]; self->_videoRenderer = [[VideoDecoderRenderer alloc] initWithView:self->_renderView
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:renderer connectionCallbacks:self->_callbacks]; callbacks:self->_callbacks
streamAspectRatio:(float)self->_config.width / (float)self->_config.height
useFramePacing:self->_config.useFramePacing];
self->_connection = [[Connection alloc] initWithConfig:self->_config renderer:self->_videoRenderer connectionCallbacks:self->_callbacks];
NSOperationQueue* opQueue = [[NSOperationQueue alloc] init]; NSOperationQueue* opQueue = [[NSOperationQueue alloc] init];
[opQueue addOperation:self->_connection]; [opQueue addOperation:self->_connection];
}); });
+2
View File
@@ -14,6 +14,8 @@
@interface VideoDecoderRenderer : NSObject @interface VideoDecoderRenderer : NSObject
@property (nonatomic, strong, readonly) AVSampleBufferDisplayLayer *displayLayer;
- (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing; - (id)initWithView:(UIView*)view callbacks:(id<ConnectionCallbacks>)callbacks streamAspectRatio:(float)aspectRatio useFramePacing:(BOOL)useFramePacing;
- (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate; - (void)setupWithVideoFormat:(int)videoFormat width:(int)videoWidth height:(int)videoHeight frameRate:(int)frameRate;
+14 -14
View File
@@ -24,7 +24,7 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
id<ConnectionCallbacks> _callbacks; id<ConnectionCallbacks> _callbacks;
float _streamAspectRatio; float _streamAspectRatio;
AVSampleBufferDisplayLayer* displayLayer; AVSampleBufferDisplayLayer* _displayLayer;
int videoFormat; int videoFormat;
int frameRate; int frameRate;
@@ -39,10 +39,10 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
- (void)reinitializeDisplayLayer - (void)reinitializeDisplayLayer
{ {
CALayer *oldLayer = displayLayer; CALayer *oldLayer = _displayLayer;
displayLayer = [[AVSampleBufferDisplayLayer alloc] init]; _displayLayer = [[AVSampleBufferDisplayLayer alloc] init];
displayLayer.backgroundColor = [UIColor blackColor].CGColor; _displayLayer.backgroundColor = [UIColor blackColor].CGColor;
// Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio // Ensure the AVSampleBufferDisplayLayer is sized to preserve the aspect ratio
// of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that // of the video stream. We used to use AVLayerVideoGravityResizeAspect, but that
@@ -55,20 +55,20 @@ extern int ff_isom_write_av1c(AVIOContext *pb, const uint8_t *buf, int size,
} else { } else {
videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio); videoSize = CGSizeMake(_view.bounds.size.width, _view.bounds.size.width / _streamAspectRatio);
} }
displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds)); _displayLayer.position = CGPointMake(CGRectGetMidX(_view.bounds), CGRectGetMidY(_view.bounds));
displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height); _displayLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);
displayLayer.videoGravity = AVLayerVideoGravityResize; _displayLayer.videoGravity = AVLayerVideoGravityResize;
// Hide the layer until we get an IDR frame. This ensures we // Hide the layer until we get an IDR frame. This ensures we
// can see the loading progress label as the stream is starting. // can see the loading progress label as the stream is starting.
displayLayer.hidden = YES; _displayLayer.hidden = YES;
if (oldLayer != nil) { if (oldLayer != nil) {
// Switch out the old display layer with the new one // Switch out the old display layer with the new one
[_view.layer replaceSublayer:oldLayer with:displayLayer]; [_view.layer replaceSublayer:oldLayer with:_displayLayer];
} }
else { else {
[_view.layer addSublayer:displayLayer]; [_view.layer addSublayer:_displayLayer];
} }
if (formatDesc != nil) { if (formatDesc != nil) {
@@ -522,8 +522,8 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
} }
// Check for previous decoder errors before doing anything // Check for previous decoder errors before doing anything
if (displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) { if (_displayLayer.status == AVQueuedSampleBufferRenderingStatusFailed) {
Log(LOG_E, @"Display layer rendering failed: %@", displayLayer.error); Log(LOG_E, @"Display layer rendering failed: %@", _displayLayer.error);
// Recreate the display layer. We are already on the main thread, // Recreate the display layer. We are already on the main thread,
// so this is safe to do right here. // so this is safe to do right here.
@@ -601,11 +601,11 @@ int DrSubmitDecodeUnit(PDECODE_UNIT decodeUnit);
} }
// Enqueue the next frame // Enqueue the next frame
[self->displayLayer enqueueSampleBuffer:sampleBuffer]; [self->_displayLayer enqueueSampleBuffer:sampleBuffer];
if (du->frameType == FRAME_TYPE_IDR) { if (du->frameType == FRAME_TYPE_IDR) {
// Ensure the layer is visible now // Ensure the layer is visible now
self->displayLayer.hidden = NO; self->_displayLayer.hidden = NO;
// Tell our parent VC to hide the progress indicator // Tell our parent VC to hide the progress indicator
[self->_callbacks videoContentShown]; [self->_callbacks videoContentShown];
@@ -1147,6 +1147,21 @@ static NSMutableSet* hostList;
// If we moved back to the center position, we should save the settings // If we moved back to the center position, we should save the settings
SettingsViewController* settingsViewController = (SettingsViewController*)[revealController rearViewController]; SettingsViewController* settingsViewController = (SettingsViewController*)[revealController rearViewController];
settingsViewController.mainFrameViewController = self; settingsViewController.mainFrameViewController = self;
// enable / disable widgets acoordingly: in streamview, disable, outside of streamview, enable.
[settingsViewController.resolutionSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.framerateSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController widget:settingsViewController.bitrateSlider setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.optimizeSettingsSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.audioOnPCSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.codecSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.yuv444Selector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.pipSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.hdrSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.framePacingSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.btMouseSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.externalDisplayModeSelector setEnabled:!self.settingsExpandedInStreamView];
[settingsViewController.backToStreamingButton setEnabled:self.settingsExpandedInStreamView];
// [settingsViewController.unlockDisplayOrientationSelector setEnabled:!self.settingsExpandedInStreamView && [self isFullScreenRequired]];//need "requires fullscreen" enabled in the app bunddle to make runtime orientation limitation woring
if (position == FrontViewPositionLeft) { if (position == FrontViewPositionLeft) {
[settingsViewController saveSettings]; [settingsViewController saveSettings];
@@ -1264,8 +1279,8 @@ static NSMutableSet* hostList;
- (bool)isIPhonePortrait{ - (bool)isIPhonePortrait{
bool isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone); bool isIPhone = ([UIDevice currentDevice].userInterfaceIdiom == UIUserInterfaceIdiomPhone);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]); CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
bool isPortrait = screenHeightInPoints > screenWidthInPoints; bool isPortrait = screenHeightInPoints > screenWidthInPoints;
return isIPhone && isPortrait; return isIPhone && isPortrait;
// return isPortrait; // return isPortrait;
@@ -1499,8 +1514,8 @@ static NSMutableSet* hostList;
_boxArtCache = [[NSCache alloc] init]; _boxArtCache = [[NSCache alloc] init];
//recordedScreenWidth = CGRectGetWidth([[UIScreen mainScreen] bounds]); //recordedScreenWidth = CGRectGetWidth([[UIScreen mainScreen] bounds]);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]); CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
// deal with scroll host view reload after device orientation change here: // deal with scroll host view reload after device orientation change here:
bool isLandscape = screenWidthInPoints > screenHeightInPoints; bool isLandscape = screenWidthInPoints > screenHeightInPoints;
@@ -1573,8 +1588,8 @@ static NSMutableSet* hostList;
CGFloat screenScale = window.screen.scale; CGFloat screenScale = window.screen.scale;
CGFloat appWindowWidth = CGRectGetWidth(window.frame) * screenScale; CGFloat appWindowWidth = CGRectGetWidth(window.frame) * screenScale;
CGFloat appWindowHeight = CGRectGetHeight(window.frame) * screenScale; CGFloat appWindowHeight = CGRectGetHeight(window.frame) * screenScale;
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]); CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
if(currentSettings.externalDisplayMode.intValue == 1 && UIScreen.screens.count > 1){ if(currentSettings.externalDisplayMode.intValue == 1 && UIScreen.screens.count > 1){
CGRect bounds = [UIScreen.screens.lastObject bounds]; CGRect bounds = [UIScreen.screens.lastObject bounds];
@@ -800,8 +800,8 @@ const int FrontViewPositionNone = 0xff;
} }
- (UIInterfaceOrientationMask)getCurrentOrientation{ - (UIInterfaceOrientationMask)getCurrentOrientation{
CGFloat screenHeightInPoints = CGRectGetHeight([[UIScreen mainScreen] bounds]); CGFloat screenHeightInPoints = CGRectGetHeight(self.view.bounds);
CGFloat screenWidthInPoints = CGRectGetWidth([[UIScreen mainScreen] bounds]); CGFloat screenWidthInPoints = CGRectGetWidth(self.view.bounds);
//lock the orientation accordingly after streaming is started //lock the orientation accordingly after streaming is started
if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape; if(screenWidthInPoints > screenHeightInPoints) return UIInterfaceOrientationMaskLandscape;
else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown; else return UIInterfaceOrientationMaskPortrait|UIInterfaceOrientationMaskPortraitUpsideDown;
@@ -14,18 +14,23 @@
#import "StreamView.h" #import "StreamView.h"
#import "LayoutOnScreenControlsViewController.h" #import "LayoutOnScreenControlsViewController.h"
#import "MainFrameViewController.h" #import "MainFrameViewController.h"
#import "StreamManager.h"
#import <AVKit/AVKit.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#if TARGET_OS_TV #if TARGET_OS_TV
@import GameController; @import GameController;
@interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate> @interface StreamFrameViewController : GCEventViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, AVPictureInPictureControllerDelegate>
#else #else
@interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolboxSpecialEntryDelegate> @interface StreamFrameViewController : UIViewController <ConnectionCallbacks, ControllerSupportDelegate, UserInteractionDelegate, UIScrollViewDelegate, ToolboxSpecialEntryDelegate, AVPictureInPictureControllerDelegate>
#endif #endif
@property (nonatomic) StreamManager* streamMan;
@property (nonatomic) StreamConfiguration* streamConfig; @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, assign) MainFrameViewController *mainFrameViewcontroller;
@@ -9,10 +9,12 @@
// Copyright © 2024 True砖家 @ Bilibili. All rights reserved. // Copyright © 2024 True砖家 @ Bilibili. All rights reserved.
// //
#import <AVKit/AVKit.h>
#import "StreamFrameViewController.h" #import "StreamFrameViewController.h"
#import "MainFrameViewController.h" #import "MainFrameViewController.h"
#import "VideoDecoderRenderer.h" #import "VideoDecoderRenderer.h"
#import "StreamManager.h" #import "StreamManager.h"
#import "SceneDelegate.h"
#import "ControllerSupport.h" #import "ControllerSupport.h"
#import "DataManager.h" #import "DataManager.h"
#import "CustomEdgeSlideGestureRecognizer.h" #import "CustomEdgeSlideGestureRecognizer.h"
@@ -69,6 +71,8 @@
*/ */
UIWindow *_deviceWindow; UIWindow *_deviceWindow;
dispatch_block_t _delayedRemoveExtScreen; dispatch_block_t _delayedRemoveExtScreen;
VideoDecoderRenderer *_videoRenderer;
BOOL _isRestoringFromPiP;
#if !TARGET_OS_TV #if !TARGET_OS_TV
CustomEdgeSlideGestureRecognizer *_slideToSettingsRecognizer; CustomEdgeSlideGestureRecognizer *_slideToSettingsRecognizer;
CustomEdgeSlideGestureRecognizer *_slideToCmdToolRecognizer; CustomEdgeSlideGestureRecognizer *_slideToCmdToolRecognizer;
@@ -76,6 +80,61 @@
LayoutOnScreenControlsViewController *_layoutOnScreenControlsVC; LayoutOnScreenControlsViewController *_layoutOnScreenControlsVC;
ToolboxViewController* toolBoxViewController; ToolboxViewController* toolBoxViewController;
#endif #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 { - (BOOL)isFirstLaunch {
@@ -95,6 +154,52 @@
return (_settings.touchMode.intValue == RelativeTouch || _settings.touchMode.intValue == NativeTouch || _settings.touchMode.intValue == AbsoluteTouch) && _settings.onscreenControls.intValue == OnScreenControlsLevelCustom; return (_settings.touchMode.intValue == RelativeTouch || _settings.touchMode.intValue == NativeTouch || _settings.touchMode.intValue == AbsoluteTouch) && _settings.onscreenControls.intValue == OnScreenControlsLevelCustom;
} }
- (void)setupPiPControllerWithRenderer:(VideoDecoderRenderer *)videoRenderer { // Ensure we have the renderer and its layer
if (self.pipController) {
return;
}
Log(LOG_I, @"Setting up PiP controller...");
if (!videoRenderer || !videoRenderer.displayLayer) {
Log(LOG_E, @"PiP setup failed: Video renderer or display layer not ready.");
return;
}
AVSampleBufferDisplayLayer *streamLayer = videoRenderer.displayLayer;
if ([AVPictureInPictureController isPictureInPictureSupported]) {
if (@available(iOS 15.0, *)) {
self.pipContentSource = [[AVPictureInPictureControllerContentSource alloc] initWithSampleBufferDisplayLayer:streamLayer playbackDelegate:self];
self.pipController = [[AVPictureInPictureController alloc] initWithContentSource:self.pipContentSource];
self.pipController.canStartPictureInPictureAutomaticallyFromInline = YES;
} else {
Log(LOG_E, @"PiP not fully supported on this device.");
return;
}
if (self.pipController) {
self.pipController.delegate = self;
Log(LOG_I, @"PiP controller created successfully.");
} else {
Log(LOG_E, @"Failed to create PiP controller.");
}
} else {
Log(LOG_E, @"PiP not supported on this device.");
}
}
- (void)cleanupPiPController {
if (self.pipController) {
self.pipController.delegate = nil;
self.pipController = nil;
if (@available(iOS 15.0, *)) {
self.pipContentSource = nil;
}
Log(LOG_I, @"PiP controller cleaned up.");
}
}
- (void)configOscLayoutTool{ - (void)configOscLayoutTool{
if([self isOscLayoutToolEnabled]){ if([self isOscLayoutToolEnabled]){
if(![toolBoxViewController.specialEntries containsObject:@"widgetLayoutTool"]) [toolBoxViewController.specialEntries insertObject:@"widgetLayoutTool" atIndex:0]; if(![toolBoxViewController.specialEntries containsObject:@"widgetLayoutTool"]) [toolBoxViewController.specialEntries insertObject:@"widgetLayoutTool" atIndex:0];
@@ -224,9 +329,9 @@
[super viewDidAppear:animated]; [super viewDidAppear:animated];
_deviceWindow = self.view.window; _deviceWindow = self.view.window;
UIScreen *currentScreen = self.view.window.windowScene.screen;
if (UIScreen.screens.count > 1 && [self isAirPlayEnabled]) { if (UIScreen.screens.count > 1 && [self isAirPlayEnabled] && currentScreen == UIScreen.mainScreen) {
[self prepExtScreen:UIScreen.screens.lastObject]; [SceneDelegate setExternalDisplayRenderView:self->_streamVideoRenderView];
} }
else { else {
/* /*
@@ -382,6 +487,8 @@
toolBoxViewController = [[ToolboxViewController alloc] init]; toolBoxViewController = [[ToolboxViewController alloc] init];
toolBoxViewController.specialEntryDelegate = self; toolBoxViewController.specialEntryDelegate = self;
_isRestoringFromPiP = NO;
/* /*
_settings.externalDisplayMode.intValue: _settings.externalDisplayMode.intValue:
0 - stage manager 0 - stage manager
@@ -642,6 +749,11 @@
- (void) returnToMainFrame { - (void) returnToMainFrame {
// Reset display mode back to default // Reset display mode back to default
[self updatePreferredDisplayMode:NO]; [self updatePreferredDisplayMode:NO];
[SceneDelegate clearExternalDisplayRenderView];
if (_settings.enablePIP) {
[self cleanupPiPController];
}
[_statsUpdateTimer invalidate]; [_statsUpdateTimer invalidate];
_statsUpdateTimer = nil; _statsUpdateTimer = nil;
@@ -654,19 +766,34 @@
// External Screen connected // External Screen connected
- (void)extScreenDidConnect:(NSNotification *)notification { - (void)extScreenDidConnect:(NSNotification *)notification {
Log(LOG_I, @"External Screen Connected"); Log(LOG_I, @"External Screen Connected");
dispatch_async(dispatch_get_main_queue(), ^{ if ([self isAirPlayEnabled] && [notification.object isKindOfClass:[UIScreen class]]) {
[self prepExtScreen:notification.object]; 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 // External Screen disconnected
- (void)extScreenDidDisconnect:(NSNotification *)notification { - (void)extScreenDidDisconnect:(NSNotification *)notification {
Log(LOG_I, @"External Screen Disconnected"); Log(LOG_I, @"External Screen Disconnected");
if(UIScreen.screens.count < 2) if(UIScreen.screens.count < 2) {
{ [SceneDelegate clearExternalDisplayRenderView];
dispatch_async(dispatch_get_main_queue(), ^{ // Add the render view back to the local StreamView if AirPlay was active
[self removeExtScreen]; if ([self isAirPlayEnabled]) {
}); if (_streamVideoRenderView && _streamView) {
[_streamView insertSubview:_streamVideoRenderView atIndex:0];
[self handleViewResize]; // Adjust frames as needed
}
}
NSNotificationCenter* nc = [NSNotificationCenter defaultCenter];
[nc postNotificationName:@"ScreenChanged" object:self]; // Your existing notification
} }
} }
@@ -675,7 +802,10 @@
} }
- (BOOL) isAirPlaying{ - (BOOL) isAirPlaying{
return _extWindow != nil && _extWindow.hidden == NO; if (_settings.externalDisplayMode.intValue == 1 && _streamVideoRenderView) {
return _streamVideoRenderView.hidden;
}
return NO;
} }
- (BOOL) isAirPlayEnabled{ - (BOOL) isAirPlayEnabled{
@@ -685,45 +815,12 @@
- (void) reloadAirPlayConfig{ - (void) reloadAirPlayConfig{
if (UIScreen.screens.count == 1){return;} if (UIScreen.screens.count == 1){return;}
if (![self isAirPlaying] && [self isAirPlayEnabled]){ if (![self isAirPlaying] && [self isAirPlayEnabled]){
[self prepExtScreen:UIScreen.screens.lastObject]; [SceneDelegate setExternalDisplayRenderView:_streamVideoRenderView];
}else if ([self isAirPlaying] && ![self isAirPlayEnabled]){ }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{ - (void) handleViewResize{
_streamView.bounds = _deviceWindow.bounds; _streamView.bounds = _deviceWindow.bounds;
_streamView.frame = _deviceWindow.frame; _streamView.frame = _deviceWindow.frame;
@@ -739,8 +836,18 @@
// This will fire if the user opens control center or gets a low battery message // This will fire if the user opens control center or gets a low battery message
- (void)applicationWillResignActive:(NSNotification *)notification { - (void)applicationWillResignActive:(NSNotification *)notification {
if (_settings.enablePIP && self.pipController && !self.pipController.isPictureInPictureActive && self.pipController.isPictureInPicturePossible) {
VideoDecoderRenderer *renderer = self.streamMan.videoRenderer;
if (renderer && renderer.displayLayer && self.view.window) {
Log(LOG_I, @"Starting Picture in Picture");
[self.pipController startPictureInPicture];
} else {
Log(LOG_I, @"Skipping PiP start: Renderer/Layer/Window not ready OR PiP not possible.");
}
}
if (_inactivityTimer != nil) { if (_inactivityTimer != nil) {
[_inactivityTimer invalidate]; [_inactivityTimer invalidate];
_inactivityTimer = nil;
} }
#if !TARGET_OS_TV #if !TARGET_OS_TV
@@ -769,6 +876,9 @@
[_inactivityTimer invalidate]; [_inactivityTimer invalidate];
_inactivityTimer = nil; _inactivityTimer = nil;
} }
if (self.pipController && self.pipController.isPictureInPictureActive) {
[self.pipController stopPictureInPicture];
}
} }
// This fires when the home button is pressed // This fires when the home button is pressed
@@ -782,6 +892,21 @@
} }
// [self returnToMainFrame]; // [self returnToMainFrame];
// Don't terminate if pip is active
if (_settings.enablePIP && self.pipController && self.pipController.isPictureInPictureActive) {
Log(LOG_I, @"PIP is active, not terminating stream");
return;
}
#if !TARGET_OS_TV
// Terminate the stream if the app is inactive for 60 seconds
Log(LOG_I, @"Starting inactivity termination timer");
_inactivityTimer = [NSTimer scheduledTimerWithTimeInterval:60
target:self
selector:@selector(inactiveTimerExpired:)
userInfo:nil
repeats:NO];
#endif
} }
- (void)expandSettingsView{ - (void)expandSettingsView{
@@ -1059,8 +1184,21 @@
} }
- (void) videoContentShown { - (void) videoContentShown {
[_spinner stopAnimating]; dispatch_async(dispatch_get_main_queue(), ^{
[self.view setBackgroundColor:[UIColor blackColor]]; [_spinner stopAnimating];
[self.view setBackgroundColor:[UIColor blackColor]];
if (@available(iOS 15.0, *)) {
if (_settings.enablePIP) {
if (self->_streamMan && self->_streamMan.videoRenderer) {
Log(LOG_I, @"Setting up PiP with renderer: %p", self->_streamMan.videoRenderer);
[self setupPiPControllerWithRenderer:self->_streamMan.videoRenderer];
} else {
Log(LOG_I, @"No renderer available for PiP setup");
}
}
}
});
} }
- (void)didReceiveMemoryWarning - (void)didReceiveMemoryWarning