#import #import #import "Header.h" #import "../YouTubeHeader/GIMBindingBuilder.h" #import "../YouTubeHeader/GPBExtensionRegistry.h" #import "../YouTubeHeader/MLPIPController.h" #import "../YouTubeHeader/MLDefaultPlayerViewFactory.h" #import "../YouTubeHeader/YTBackgroundabilityPolicy.h" #import "../YouTubeHeader/YTMainAppControlsOverlayView.h" #import "../YouTubeHeader/YTMainAppVideoPlayerOverlayViewController.h" #import "../YouTubeHeader/YTHotConfig.h" #import "../YouTubeHeader/YTLocalPlaybackController.h" #import "../YouTubeHeader/YTPlayerPIPController.h" #import "../YouTubeHeader/YTSettingsSectionItem.h" #import "../YouTubeHeader/YTSettingsSectionItemManager.h" #import "../YouTubeHeader/YTIPictureInPictureRendererRoot.h" #import "../YouTubeHeader/YTColor.h" #import "../YouTubeHeader/QTMIcon.h" #import "../YouTubeHeader/YTSlimVideoScrollableActionBarCellController.h" #import "../YouTubeHeader/YTSlimVideoScrollableDetailsActionsView.h" #import "../YouTubeHeader/YTSlimVideoDetailsActionView.h" #import "../YouTubeHeader/YTISlimMetadataButtonSupportedRenderers.h" #import "../YouTubeHeader/YTPageStyleController.h" #import "../YouTubeHeader/YTPlayerStatus.h" #import "../YouTubeHeader/YTWatchViewController.h" #define PiPButtonType 801 @interface YTMainAppControlsOverlayView (YP) @property (retain, nonatomic) YTQTMButton *pipButton; - (void)didPressPiP:(id)arg; - (UIImage *)pipImage; @end BOOL FromUser = NO; BOOL PiPDisabled = NO; extern BOOL LegacyPiP(); extern YTHotConfig *(*InjectYTHotConfig)(void); BOOL TweakEnabled() { return [[NSUserDefaults standardUserDefaults] boolForKey:EnabledKey]; } BOOL UsePiPButton() { return [[NSUserDefaults standardUserDefaults] boolForKey:PiPActivationMethodKey]; } BOOL NoMiniPlayerPiP() { return [[NSUserDefaults standardUserDefaults] boolForKey:NoMiniPlayerPiPKey]; } BOOL UseTabBarPiPButton() { return [[NSUserDefaults standardUserDefaults] boolForKey:PiPActivationMethod2Key]; } BOOL NonBackgroundable() { return [[NSUserDefaults standardUserDefaults] boolForKey:NonBackgroundableKey]; } BOOL FakeVersion() { return [[NSUserDefaults standardUserDefaults] boolForKey:FakeVersionKey]; } BOOL isPictureInPictureActive(MLPIPController *pip) { return [pip respondsToSelector:@selector(pictureInPictureActive)] ? [pip pictureInPictureActive] : [pip isPictureInPictureActive]; } static NSString *PiPIconPath; static NSString *TabBarPiPIconPath; static NSString *PiPVideoPath; static void forcePictureInPicture(YTHotConfig *hotConfig, BOOL value) { [hotConfig mediaHotConfig].enablePictureInPicture = value; YTIIosMediaHotConfig *iosMediaHotConfig = hotConfig.hotConfigGroup.mediaHotConfig.iosMediaHotConfig; iosMediaHotConfig.enablePictureInPicture = value; if ([iosMediaHotConfig respondsToSelector:@selector(setEnablePipForNonBackgroundableContent:)]) iosMediaHotConfig.enablePipForNonBackgroundableContent = value && NonBackgroundable(); if ([iosMediaHotConfig respondsToSelector:@selector(setEnablePipForNonPremiumUsers:)]) iosMediaHotConfig.enablePipForNonPremiumUsers = value; } static void activatePiPBase(YTPlayerPIPController *controller, BOOL playPiP) { MLPIPController *pip = [controller valueForKey:@"_pipController"]; if ([controller respondsToSelector:@selector(maybeEnablePictureInPicture)]) [controller maybeEnablePictureInPicture]; else if ([controller respondsToSelector:@selector(maybeInvokePictureInPicture)]) [controller maybeInvokePictureInPicture]; else { BOOL canPiP = [controller respondsToSelector:@selector(canEnablePictureInPicture)] && [controller canEnablePictureInPicture]; if (!canPiP) canPiP = [controller respondsToSelector:@selector(canInvokePictureInPicture)] && [controller canInvokePictureInPicture]; if (canPiP) { if ([pip respondsToSelector:@selector(activatePiPController)]) [pip activatePiPController]; else [pip startPictureInPicture]; } } AVPictureInPictureController *avpip = [pip valueForKey:@"_pictureInPictureController"]; if (playPiP) { if ([avpip isPictureInPicturePossible]) [avpip startPictureInPicture]; } else { if ([pip respondsToSelector:@selector(deactivatePiPController)]) [pip deactivatePiPController]; else [avpip stopPictureInPicture]; } } static void activatePiP(YTLocalPlaybackController *local, BOOL playPiP) { if (![local isKindOfClass:%c(YTLocalPlaybackController)]) return; YTPlayerPIPController *controller = [local valueForKey:@"_playerPIPController"]; activatePiPBase(controller, playPiP); } static void bootstrapPiP(YTPlayerViewController *self, BOOL playPiP) { YTHotConfig *hotConfig; @try { if (InjectYTHotConfig) hotConfig = InjectYTHotConfig(); else hotConfig = [self valueForKey:@"_hotConfig"]; } @catch (id ex) { hotConfig = [[self gimme] instanceForType:%c(YTHotConfig)]; } forcePictureInPicture(hotConfig, YES); YTLocalPlaybackController *local = [self valueForKey:@"_playbackController"]; activatePiP(local, playPiP); } #pragma mark - Video tab bar PiP Button static YTISlimMetadataButtonSupportedRenderers *makeUnderPlayerButton(NSString *title, int iconType, NSString *browseId) { YTISlimMetadataButtonSupportedRenderers *supportedRenderer = [[%c(YTISlimMetadataButtonSupportedRenderers) alloc] init]; YTISlimMetadataButtonRenderer *metadataButtonRenderer = [[%c(YTISlimMetadataButtonRenderer) alloc] init]; YTIButtonSupportedRenderers *buttonSupportedRenderer = [[%c(YTIButtonSupportedRenderers) alloc] init]; YTIBrowseEndpoint *endPoint = [[%c(YTIBrowseEndpoint) alloc] init]; YTICommand *command = [[%c(YTICommand) alloc] init]; YTIButtonRenderer *button = [[%c(YTIButtonRenderer) alloc] init]; YTIIcon *icon = [[%c(YTIIcon) alloc] init]; endPoint.browseId = browseId; command.browseEndpoint = endPoint; icon.iconType = iconType; button.style = 8; // Opacity style button.tooltip = title; button.size = 1; // Default size button.isDisabled = NO; button.text = [%c(YTIFormattedString) formattedStringWithString:title]; button.icon = icon; button.navigationEndpoint = command; buttonSupportedRenderer.buttonRenderer = button; metadataButtonRenderer.button = buttonSupportedRenderer; supportedRenderer.slimMetadataButtonRenderer = metadataButtonRenderer; return supportedRenderer; } %hook YTIIcon - (UIImage *)iconImageWithColor:(UIColor *)color { if (self.iconType == PiPButtonType) { UIImage *image = [%c(QTMIcon) tintImage:[UIImage imageWithContentsOfFile:TabBarPiPIconPath] color:[[%c(YTPageStyleController) currentColorPalette] textPrimary]]; if ([image respondsToSelector:@selector(imageFlippedForRightToLeftLayoutDirection)]) image = [image imageFlippedForRightToLeftLayoutDirection]; return image; } return %orig; } %end %hook YTSlimVideoScrollableDetailsActionsView - (void)createActionViewsFromSupportedRenderers:(NSMutableArray *)renderers { // for old YouTube version if (UseTabBarPiPButton()) { YTISlimMetadataButtonSupportedRenderers *PiPButton = makeUnderPlayerButton(@"PiP", PiPButtonType, @"YouPiP.pip.command"); if (![renderers containsObject:PiPButton]) [renderers addObject:PiPButton]; } %orig; } - (void)createActionViewsFromSupportedRenderers:(NSMutableArray *)renderers withElementsContextBlock:(id)arg2 { if (UseTabBarPiPButton()) { YTISlimMetadataButtonSupportedRenderers *PiPButton = makeUnderPlayerButton(@"PiP", PiPButtonType, @"YouPiP.pip.command"); if (![renderers containsObject:PiPButton]) [renderers addObject:PiPButton]; } %orig; } %end %hook YTSlimVideoDetailsActionView - (void)didTapButton:(id)arg1 { if ([self.label.attributedText.string isEqualToString:@"PiP"]) { YTSlimVideoScrollableActionBarCellController *_delegate = self.delegate; YTPlayerViewController *playerViewController = nil; @try { if ([[_delegate valueForKey:@"_metadataPanelStateProvider"] isKindOfClass:%c(YTWatchController)]) { id provider = [_delegate valueForKey:@"_metadataPanelStateProvider"]; @try { YTWatchViewController *watchViewController = [provider valueForKey:@"_watchViewController"]; playerViewController = [watchViewController valueForKey:@"_playerViewController"]; } @catch (id ex) { playerViewController = [provider valueForKey:@"_playerViewController"]; } } } @catch (id ex) { // for old YouTube version if ([[_delegate valueForKey:@"_ngwMetadataPanelStateProvider"] isKindOfClass:%c(YTNGWatchController)]) { id provider = [_delegate valueForKey:@"_ngwMetadataPanelStateProvider"]; playerViewController = [provider valueForKey:@"_playerViewController"]; } } if (playerViewController && [playerViewController isKindOfClass:%c(YTPlayerViewController)]) { FromUser = YES; bootstrapPiP(playerViewController, YES); } return; } %orig; } %end #pragma mark - Overlay PiP Button %hook YTMainAppVideoPlayerOverlayViewController - (void)updateTopRightButtonAvailability { %orig; YTMainAppVideoPlayerOverlayView *v = [self videoPlayerOverlayView]; YTMainAppControlsOverlayView *c = [v valueForKey:@"_controlsOverlayView"]; c.pipButton.hidden = !UsePiPButton(); [c setNeedsLayout]; } %end static void createPiPButton(YTMainAppControlsOverlayView *self) { if (self) { CGFloat padding = [[self class] topButtonAdditionalPadding]; UIImage *image = [self pipImage]; self.pipButton = [self buttonWithImage:image accessibilityLabel:@"pip" verticalContentPadding:padding]; self.pipButton.hidden = YES; self.pipButton.alpha = 0; [self.pipButton addTarget:self action:@selector(didPressPiP:) forControlEvents:UIControlEventTouchUpInside]; @try { [[self valueForKey:@"_topControlsAccessibilityContainerView"] addSubview:self.pipButton]; } @catch (id ex) { [self addSubview:self.pipButton]; } } } static NSMutableArray *topControls(YTMainAppControlsOverlayView *self, NSMutableArray *controls) { if (UsePiPButton()) [controls insertObject:self.pipButton atIndex:0]; return controls; } %hook YTMainAppControlsOverlayView %property (retain, nonatomic) YTQTMButton *pipButton; - (id)initWithDelegate:(id)delegate { self = %orig; createPiPButton(self); return self; } - (id)initWithDelegate:(id)delegate autoplaySwitchEnabled:(BOOL)autoplaySwitchEnabled { self = %orig; createPiPButton(self); return self; } - (NSMutableArray *)topButtonControls { return topControls(self, %orig); } - (NSMutableArray *)topControls { return topControls(self, %orig); } - (void)setTopOverlayVisible:(BOOL)visible isAutonavCanceledState:(BOOL)canceledState { if (UsePiPButton()) self.pipButton.alpha = canceledState || !visible ? 0.0 : 1.0; %orig; } %new(@:) - (UIImage *)pipImage { static UIImage *image = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ UIColor *color = [%c(YTColor) white1]; image = [%c(QTMIcon) tintImage:[UIImage imageWithContentsOfFile:PiPIconPath] color:color]; if ([image respondsToSelector:@selector(imageFlippedForRightToLeftLayoutDirection)]) image = [image imageFlippedForRightToLeftLayoutDirection]; }); return image; } %new(v@:@) - (void)didPressPiP:(id)arg { YTMainAppVideoPlayerOverlayViewController *c = [self valueForKey:@"_eventsDelegate"]; FromUser = YES; bootstrapPiP([c delegate], YES); } %end #pragma mark - PiP Support %hook AVPictureInPictureController + (BOOL)isPictureInPictureSupported { return YES; } %end %hook AVPlayerController - (BOOL)isPictureInPictureSupported { return YES; } %end %hook AVSampleBufferDisplayLayerPlayerController - (void)setPictureInPictureAvailable:(BOOL)available { %orig(YES); } %end %hook MLPIPController - (void)activatePiPController { %orig; if (!IS_IOS_OR_NEWER(iOS_15_0) && !LegacyPiP()) { MLHAMSBDLSampleBufferRenderingView *view = [self valueForKey:@"_HAMPlayerView"]; CGSize size = [self renderSizeForView:view]; AVPictureInPictureController *avpip = [self valueForKey:@"_pictureInPictureController"]; [avpip sampleBufferDisplayLayerRenderSizeDidChangeToSize:size]; [avpip sampleBufferDisplayLayerDidAppear]; } } - (BOOL)isPictureInPictureSupported { return YES; } %new(v@:@) - (BOOL)pictureInPictureControllerPlaybackPaused:(AVPictureInPictureController *)pictureInPictureController { return [self pictureInPictureControllerIsPlaybackPaused:pictureInPictureController]; } %new(v@:@) - (void)pictureInPictureControllerStartPlayback:(id)arg1 { [self pictureInPictureControllerStartPlayback]; } %new(v@:@) - (void)pictureInPictureControllerStopPlayback:(id)arg1 { [self pictureInPictureControllerStopPlayback]; } %new - (void)renderingViewSampleBufferFrameSizeDidChange:(CGSize)size { if (!IS_IOS_OR_NEWER(iOS_15_0) && size.width && size.height) { AVPictureInPictureController *avpip = [self valueForKey:@"_pictureInPictureController"]; [avpip sampleBufferDisplayLayerRenderSizeDidChangeToSize:size]; } } %new(v@:@) - (void)appWillEnterForeground:(id)arg1 { if (!IS_IOS_OR_NEWER(iOS_15_0) && !LegacyPiP()) { AVPictureInPictureController *avpip = [self valueForKey:@"_pictureInPictureController"]; [avpip sampleBufferDisplayLayerDidAppear]; } } %new(v@:@) - (void)appWillEnterBackground:(id)arg1 { if (!IS_IOS_OR_NEWER(iOS_15_0) && !LegacyPiP()) { AVPictureInPictureController *avpip = [self valueForKey:@"_pictureInPictureController"]; [avpip sampleBufferDisplayLayerDidDisappear]; } } %end %hook MLDefaultPlayerViewFactory - (MLAVPlayerLayerView *)AVPlayerViewForVideo:(MLVideo *)video playerConfig:(MLInnerTubePlayerConfig *)playerConfig { forcePictureInPicture([self valueForKey:@"_hotConfig"], YES); return %orig; } %end #pragma mark - PiP Support, Backgroundable %hook YTIHamplayerConfig - (BOOL)enableBackgroundable { return YES; } %end %hook YTIBackgroundOfflineSettingCategoryEntryRenderer - (BOOL)isBackgroundEnabled { return YES; } %end %hook YTBackgroundabilityPolicy - (void)updateIsBackgroundableByUserSettings { %orig; [self setValue:@(YES) forKey:@"_backgroundableByUserSettings"]; } %end %hook YTSettingsSectionItemManager - (YTSettingsSectionItem *)pictureInPictureSectionItem { forcePictureInPicture([self valueForKey:@"_hotConfig"], YES); return %orig; } - (YTSettingsSectionItem *)pictureInPictureSectionItem:(id)arg1 { forcePictureInPicture([self valueForKey:@"_hotConfig"], YES); return %orig; } %end #pragma mark - Hacks BOOL YTSingleVideo_isLivePlayback_override = NO; %hook YTSingleVideo - (BOOL)isLivePlayback { return YTSingleVideo_isLivePlayback_override ? NO : %orig; } %end static YTHotConfig *getHotConfig(YTPlayerPIPController *self) { @try { return [self valueForKey:@"_hotConfig"]; } @catch (id ex) { return [[self valueForKey:@"_config"] valueForKey:@"_hotConfig"]; } } %hook YTPlayerPIPController - (BOOL)canInvokePictureInPicture { forcePictureInPicture(getHotConfig(self), YES); YTSingleVideo_isLivePlayback_override = YES; BOOL value = %orig; YTSingleVideo_isLivePlayback_override = NO; return value; } - (BOOL)canEnablePictureInPicture { forcePictureInPicture(getHotConfig(self), YES); YTSingleVideo_isLivePlayback_override = YES; BOOL value = %orig; YTSingleVideo_isLivePlayback_override = NO; return value; } - (void)didStopPictureInPicture { FromUser = NO; %orig; } - (void)appWillResignActive:(id)arg1 { // If PiP button on, PiP doesn't activate on app resign unless it's from user BOOL hasPiPButton = UsePiPButton() || UseTabBarPiPButton(); BOOL disablePiP = hasPiPButton && !FromUser; if (disablePiP) { MLPIPController *pip = [self valueForKey:@"_pipController"]; [pip setValue:nil forKey:@"_pictureInPictureController"]; } else { if (LegacyPiP()) activatePiPBase(self, YES); %orig; } } %end %hook YTSingleVideoController - (void)playerStatusDidChange:(YTPlayerStatus *)playerStatus { %orig; PiPDisabled = NoMiniPlayerPiP() && playerStatus.visibility == 1; } %end %hook AVPictureInPicturePlatformAdapter - (BOOL)isSystemPictureInPicturePossible { return PiPDisabled ? NO : %orig; } %end %hook YTIPlayabilityStatus - (BOOL)isPlayableInBackground { return YES; } - (BOOL)isPlayableInPictureInPicture { return YES; } - (BOOL)hasPictureInPicture { return YES; } %end #pragma mark - PiP Support, Binding %hook YTAppModule - (void)configureWithBinder:(GIMBindingBuilder *)binder { %orig; [[binder bindType:%c(MLPIPController)] initializedWith:^(id a) { MLPIPController *pip = [%c(MLPIPController) alloc]; if ([pip respondsToSelector:@selector(initWithPlaceholderPlayerItemResourcePath:)]) pip = [pip initWithPlaceholderPlayerItemResourcePath:PiPVideoPath]; else if ([pip respondsToSelector:@selector(initWithPlaceholderPlayerItem:)]) pip = [pip initWithPlaceholderPlayerItem:[AVPlayerItem playerItemWithURL:[NSURL URLWithString:PiPVideoPath]]]; return pip; }]; } %end %hook YTIInnertubeResourcesIosRoot - (GPBExtensionRegistry *)extensionRegistry { GPBExtensionRegistry *registry = %orig; [registry addExtension:[%c(YTIPictureInPictureRendererRoot) pictureInPictureRenderer]]; return registry; } %end %hook GoogleGlobalExtensionRegistry - (GPBExtensionRegistry *)extensionRegistry { GPBExtensionRegistry *registry = %orig; [registry addExtension:[%c(YTIPictureInPictureRendererRoot) pictureInPictureRenderer]]; return registry; } %end NSBundle *YouPiPBundle() { static NSBundle *bundle = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ NSString *tweakBundlePath = [[NSBundle mainBundle] pathForResource:@"YouPiP" ofType:@"bundle"]; if (tweakBundlePath) bundle = [NSBundle bundleWithPath:tweakBundlePath]; else bundle = [NSBundle bundleWithPath:ROOT_PATH_NS(@"/Library/Application Support/YouPiP.bundle")]; }); return bundle; } %ctor { if (!TweakEnabled()) return; NSBundle *tweakBundle = YouPiPBundle(); PiPVideoPath = [tweakBundle pathForResource:@"PiPPlaceholderAsset" ofType:@"mp4"]; PiPIconPath = [tweakBundle pathForResource:@"yt-pip-overlay" ofType:@"png"]; TabBarPiPIconPath = [tweakBundle pathForResource:@"yt-pip-tabbar" ofType:@"png"]; %init; }