Files
YTLitePlus/Tweaks/YouPiP/Tweak.x
2023-06-27 09:54:41 +02:00

601 lines
19 KiB
Plaintext

#import <version.h>
#import <rootless.h>
#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;
}