From 618a6c21cdbf5718648c0913504fe58f2927510c Mon Sep 17 00:00:00 2001 From: Jeff Magill <7297539+jefemagril@users.noreply.github.com> Date: Mon, 22 Jun 2026 01:59:26 -0400 Subject: [PATCH 1/4] Enhance YouMod with MediaPlayer support and remote command handling - Added MediaPlayer framework to the Makefile. - Introduced seekToTime method in Headers.h for playback control. - Implemented remote command handling for skip forward and backward functionality in Player.x, allowing users to control playback via remote commands. --- Files/Headers.h | 1 + Files/Player.x | 185 ++++++++++++++++++++++++++++++++++++++++++++++++ Makefile | 2 +- 3 files changed, 187 insertions(+), 1 deletion(-) diff --git a/Files/Headers.h b/Files/Headers.h index 649bccb8..e4b707c0 100644 --- a/Files/Headers.h +++ b/Files/Headers.h @@ -208,6 +208,7 @@ typedef NS_ENUM(NSUInteger, GestureSection) { - (void)YouModAutoFullscreen; - (void)YouModTurnOffCaptions; - (void)setActiveCaptionTrack:(id)arg1 source:(long long)arg2; +- (void)seekToTime:(CGFloat)time; - (void)setPlaybackRate:(float)rate; @end diff --git a/Files/Player.x b/Files/Player.x index 63418794..d5a94250 100644 --- a/Files/Player.x +++ b/Files/Player.x @@ -4,6 +4,114 @@ extern void YouModDownloadSetCurrentPlayer(YTPlayerViewController *player); float playbackRate = 1.0; +static const NSTimeInterval YouModRemoteSkipInterval = 10.0; +static __weak YTPlayerViewController *YouModCurrentPlayerViewController; +static NSTimeInterval YouModCurrentPlaybackTime = 0; +static NSTimeInterval YouModCurrentDuration = 0; +static BOOL YouModHasPlaybackTime = NO; +static id YouModSkipBackwardCommandTarget; +static id YouModSkipForwardCommandTarget; +static NSMutableArray *YouModRemoteCommandTargetProxies; + +static void YouModUpdateCurrentPlayer(YTPlayerViewController *player) { + YouModCurrentPlayerViewController = player; +} + +static void YouModUpdatePlaybackTime(YTPlayerViewController *player, YTSingleVideoController *video, YTSingleVideoTime *time) { + YouModUpdateCurrentPlayer(player); + YouModCurrentPlaybackTime = time.time; + YouModCurrentDuration = video.totalMediaTime; + YouModHasPlaybackTime = YES; +} + +static BOOL YouModIsPreviousNextCommand(MPRemoteCommand *command) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + return command == commandCenter.previousTrackCommand || command == commandCenter.nextTrackCommand; +} + +static BOOL YouModSeekByInterval(NSTimeInterval interval) { + if (!IS_ENABLED(ReplacePrevNextButtons) || !YouModCurrentPlayerViewController || !YouModHasPlaybackTime) return NO; + if (![YouModCurrentPlayerViewController respondsToSelector:@selector(seekToTime:)]) return NO; + + NSTimeInterval targetTime = YouModCurrentPlaybackTime + interval; + if (YouModCurrentDuration > 0) targetTime = fmin(targetTime, YouModCurrentDuration); + targetTime = fmax(targetTime, 0); + + [YouModCurrentPlayerViewController seekToTime:(CGFloat)targetTime]; + YouModCurrentPlaybackTime = targetTime; + return YES; +} + +static MPRemoteCommandHandlerStatus YouModStatusForSeek(BOOL handled) { + return handled ? MPRemoteCommandHandlerStatusSuccess : MPRemoteCommandHandlerStatusNoActionableNowPlayingItem; +} + +static BOOL YouModHandlePreviousNextRemoteCommand(MPRemoteCommandEvent *event) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + if (event.command == commandCenter.previousTrackCommand) return YouModSeekByInterval(-YouModRemoteSkipInterval); + if (event.command == commandCenter.nextTrackCommand) return YouModSeekByInterval(YouModRemoteSkipInterval); + return NO; +} + +static BOOL YouModIsPreviousNextRemoteCommandEvent(MPRemoteCommandEvent *event) { + return YouModIsPreviousNextCommand(event.command); +} + +@interface YouModRemoteCommandTargetProxy : NSObject +@property (nonatomic, weak) MPRemoteCommand *command; +@property (nonatomic, weak) id target; +@property (nonatomic, assign) SEL action; +- (MPRemoteCommandHandlerStatus)youModHandleRemoteCommandEvent:(MPRemoteCommandEvent *)event; +@end + +@implementation YouModRemoteCommandTargetProxy +- (MPRemoteCommandHandlerStatus)youModHandleRemoteCommandEvent:(MPRemoteCommandEvent *)event { + if (IS_ENABLED(ReplacePrevNextButtons) && YouModIsPreviousNextRemoteCommandEvent(event)) { + return YouModStatusForSeek(YouModHandlePreviousNextRemoteCommand(event)); + } + + if (!self.target || !self.action) return MPRemoteCommandHandlerStatusCommandFailed; + + NSMethodSignature *signature = [self.target methodSignatureForSelector:self.action]; + if (!signature) return MPRemoteCommandHandlerStatusCommandFailed; + + NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:signature]; + invocation.target = self.target; + invocation.selector = self.action; + if (signature.numberOfArguments > 2) [invocation setArgument:&event atIndex:2]; + [invocation invoke]; + + if (signature.methodReturnLength == 0) return MPRemoteCommandHandlerStatusSuccess; + + MPRemoteCommandHandlerStatus status = MPRemoteCommandHandlerStatusSuccess; + [invocation getReturnValue:&status]; + return status; +} +@end + +static void YouModConfigureRemoteSkipCommands(void) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + BOOL enabled = IS_ENABLED(ReplacePrevNextButtons); + NSArray *intervals = @[@(YouModRemoteSkipInterval)]; + + commandCenter.skipBackwardCommand.enabled = enabled; + commandCenter.skipBackwardCommand.preferredIntervals = intervals; + commandCenter.skipForwardCommand.enabled = enabled; + commandCenter.skipForwardCommand.preferredIntervals = intervals; + + if (!YouModSkipBackwardCommandTarget) { + YouModSkipBackwardCommandTarget = [commandCenter.skipBackwardCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + return YouModStatusForSeek(YouModSeekByInterval(-YouModRemoteSkipInterval)); + }]; + } + + if (!YouModSkipForwardCommandTarget) { + YouModSkipForwardCommandTarget = [commandCenter.skipForwardCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + return YouModStatusForSeek(YouModSeekByInterval(YouModRemoteSkipInterval)); + }]; + } +} + /* static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoController *video, YTSingleVideoTime *time) { if (!IS_ENABLED(ShowExtraTimeRemaining)) return; @@ -204,6 +312,68 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll - (BOOL)replacePreviousPaddleWithRewindButtonForSingletonVods { return IS_ENABLED(ReplacePrevNextButtons) ? YES : %orig; } %end +%hook MPRemoteCommand +- (void)addTarget:(id)target action:(SEL)action { + if (YouModIsPreviousNextCommand(self)) { + if (!YouModRemoteCommandTargetProxies) YouModRemoteCommandTargetProxies = [NSMutableArray array]; + + YouModRemoteCommandTargetProxy *proxy = [[YouModRemoteCommandTargetProxy alloc] init]; + proxy.command = self; + proxy.target = target; + proxy.action = action; + [YouModRemoteCommandTargetProxies addObject:proxy]; + %orig(proxy, @selector(youModHandleRemoteCommandEvent:)); + return; + } + + %orig; +} + +- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler { + if (YouModIsPreviousNextCommand(self)) { + return %orig(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + if (IS_ENABLED(ReplacePrevNextButtons) && YouModIsPreviousNextRemoteCommandEvent(event)) { + return YouModStatusForSeek(YouModHandlePreviousNextRemoteCommand(event)); + } + + return handler(event); + }); + } + + return %orig; +} + +- (void)removeTarget:(id)target action:(SEL)action { + if (YouModIsPreviousNextCommand(self) && YouModRemoteCommandTargetProxies.count > 0) { + NSArray *proxies = [YouModRemoteCommandTargetProxies copy]; + for (YouModRemoteCommandTargetProxy *proxy in proxies) { + BOOL targetMatches = !target || proxy.target == target; + BOOL actionMatches = !action || proxy.action == action; + if (proxy.command == self && targetMatches && actionMatches) { + %orig(proxy, @selector(youModHandleRemoteCommandEvent:)); + [YouModRemoteCommandTargetProxies removeObject:proxy]; + } + } + } + + %orig; +} + +- (void)removeTarget:(id)target { + if (YouModIsPreviousNextCommand(self) && YouModRemoteCommandTargetProxies.count > 0) { + NSArray *proxies = [YouModRemoteCommandTargetProxies copy]; + for (YouModRemoteCommandTargetProxy *proxy in proxies) { + if (proxy.command == self && (!target || proxy.target == target)) { + %orig(proxy); + [YouModRemoteCommandTargetProxies removeObject:proxy]; + } + } + } + + %orig; +} +%end + %group ForceMiniPlayer %hook YTIMiniplayerRenderer %new @@ -313,6 +483,8 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll %hook YTPlayerViewController - (void)loadWithPlayerTransition:(id)arg1 playbackConfig:(id)arg2 { %orig; + YouModUpdateCurrentPlayer(self); + YouModConfigureRemoteSkipCommands(); YouModDownloadSetCurrentPlayer(self); if (IS_ENABLED(AutoFullScreen)) [self performSelector:@selector(YouModAutoFullscreen) withObject:nil afterDelay:0.75]; // if (ytlBool(@"shortsToRegular")) [self performSelector:@selector(shortsToRegular) withObject:nil afterDelay:0.75]; @@ -321,6 +493,8 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll - (void)prepareToLoadWithPlayerTransition:(id)arg1 expectedLayout:(id)arg2 { %orig; + YouModUpdateCurrentPlayer(self); + YouModConfigureRemoteSkipCommands(); YouModDownloadSetCurrentPlayer(self); if (IS_ENABLED(AutoFullScreen)) [self performSelector:@selector(YouModAutoFullscreen) withObject:nil afterDelay:0.75]; // if (ytlBool(@"shortsToRegular")) [self performSelector:@selector(shortsToRegular) withObject:nil afterDelay:0.75]; @@ -352,6 +526,16 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll } */ +- (void)singleVideo:(YTSingleVideoController *)video currentVideoTimeDidChange:(YTSingleVideoTime *)time { + %orig; + YouModUpdatePlaybackTime(self, video, time); +} + +- (void)potentiallyMutatedSingleVideo:(YTSingleVideoController *)video currentVideoTimeDidChange:(YTSingleVideoTime *)time { + %orig; + YouModUpdatePlaybackTime(self, video, time); +} + - (void)setPlaybackRate:(float)rate { playbackRate = rate; %orig; @@ -653,6 +837,7 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll %ctor { %init; + YouModConfigureRemoteSkipCommands(); if (IS_ENABLED(OldQualityPicker)) { %init(OldVideoQuality); } diff --git a/Makefile b/Makefile index 2718ca0a..b49fa517 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,7 @@ TARGET := iphone:clang:latest:14.0 include $(THEOS)/makefiles/common.mk TWEAK_NAME = YouMod -$(TWEAK_NAME)_FRAMEWORKS = UIKit Foundation +$(TWEAK_NAME)_FRAMEWORKS = UIKit Foundation MediaPlayer $(TWEAK_NAME)_CFLAGS = -fobjc-arc $(TWEAK_NAME)_FILES = $(wildcard Files/*.x) From e4500471bc76f94bb4a70a70ec874549a450577e Mon Sep 17 00:00:00 2001 From: Jeff Magill <7297539+jefemagril@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:04:48 -0400 Subject: [PATCH 2/4] Refactor remote command handler in Player.x for improved readability - Introduced a typedef for the remote command handler to enhance code clarity. - Updated the addTargetWithHandler method to use the new typedef, streamlining the handler implementation. --- Files/Player.x | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/Files/Player.x b/Files/Player.x index d5a94250..e1125146 100644 --- a/Files/Player.x +++ b/Files/Player.x @@ -12,6 +12,7 @@ static BOOL YouModHasPlaybackTime = NO; static id YouModSkipBackwardCommandTarget; static id YouModSkipForwardCommandTarget; static NSMutableArray *YouModRemoteCommandTargetProxies; +typedef MPRemoteCommandHandlerStatus (^YouModRemoteCommandHandler)(MPRemoteCommandEvent *event); static void YouModUpdateCurrentPlayer(YTPlayerViewController *player) { YouModCurrentPlayerViewController = player; @@ -329,15 +330,16 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll %orig; } -- (id)addTargetWithHandler:(MPRemoteCommandHandlerStatus(^)(MPRemoteCommandEvent *event))handler { +- (id)addTargetWithHandler:(YouModRemoteCommandHandler)handler { if (YouModIsPreviousNextCommand(self)) { - return %orig(^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { + YouModRemoteCommandHandler wrappedHandler = ^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { if (IS_ENABLED(ReplacePrevNextButtons) && YouModIsPreviousNextRemoteCommandEvent(event)) { return YouModStatusForSeek(YouModHandlePreviousNextRemoteCommand(event)); } return handler(event); - }); + }; + return %orig(wrappedHandler); } return %orig; From 71d667e0219da44178c8afe8ddc10c695a46574a Mon Sep 17 00:00:00 2001 From: Jeff Magill <7297539+jefemagril@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:18:22 -0400 Subject: [PATCH 3/4] Update remote command handling in Player.x and improve localization strings - Enabled previous and next track commands based on the skip command state in Player.x. - Revised localization strings for clarity, changing descriptions for replacing previous/next buttons to emphasize the use of Rewind and Fast Forward controls. --- Files/Player.x | 2 ++ .../YouMod.bundle/en.lproj/Localizable.strings | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Files/Player.x b/Files/Player.x index e1125146..4c8ccfe3 100644 --- a/Files/Player.x +++ b/Files/Player.x @@ -99,6 +99,8 @@ static void YouModConfigureRemoteSkipCommands(void) { commandCenter.skipBackwardCommand.preferredIntervals = intervals; commandCenter.skipForwardCommand.enabled = enabled; commandCenter.skipForwardCommand.preferredIntervals = intervals; + commandCenter.previousTrackCommand.enabled = !enabled; + commandCenter.nextTrackCommand.enabled = !enabled; if (!YouModSkipBackwardCommandTarget) { YouModSkipBackwardCommandTarget = [commandCenter.skipBackwardCommand addTargetWithHandler:^MPRemoteCommandHandlerStatus(MPRemoteCommandEvent *event) { diff --git a/layout/Library/Application Support/YouMod.bundle/en.lproj/Localizable.strings b/layout/Library/Application Support/YouMod.bundle/en.lproj/Localizable.strings index bccaf550..15c65a6c 100644 --- a/layout/Library/Application Support/YouMod.bundle/en.lproj/Localizable.strings +++ b/layout/Library/Application Support/YouMod.bundle/en.lproj/Localizable.strings @@ -88,8 +88,8 @@ "HIDE_PREV_BUTTON_DESC" = "Hide previous/back button in the video overlay."; "HIDE_NEXT_BUTTON" = "Hide next/skip button"; "HIDE_NEXT_BUTTON_DESC" = "Hide next/skip button in the video overlay."; -"REPLACE_PREVNEXT_BUTTONS" = "Replace Previous/Next buttons"; -"REPLACE_PREVNEXT_BUTTONS_DESC" = "Replaces the Previous and Next video buttons in the player with Rewind and Fast Forward buttons."; +"REPLACE_PREVNEXT_BUTTONS" = "Use Rewind/Fast Forward controls"; +"REPLACE_PREVNEXT_BUTTONS_DESC" = "Replaces Previous and Next controls in the player and iOS media controls with Rewind and Fast Forward."; "REMOVE_DARK_OVERLAY" = "Remove dark overlay"; "REMOVE_DARK_OVERLAY_DESC" = "Remove dark overlay from the video overlay."; "HIDE_END_SCREEN" = "Hide endscreen cards"; From 529ae10c5c4de93de60faef31c8a42e9d60445ae Mon Sep 17 00:00:00 2001 From: Jeff Magill <7297539+jefemagril@users.noreply.github.com> Date: Mon, 22 Jun 2026 02:35:19 -0400 Subject: [PATCH 4/4] Add skip command handling in Player.x for enhanced remote control functionality - Introduced YouModIsSkipCommand function to identify skip forward and backward commands. - Updated setEnabled method to manage skip command states alongside previous/next commands. - Configured remote skip commands in the command handling process for improved playback control. --- Files/Player.x | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/Files/Player.x b/Files/Player.x index 4c8ccfe3..daad2f1e 100644 --- a/Files/Player.x +++ b/Files/Player.x @@ -30,6 +30,11 @@ static BOOL YouModIsPreviousNextCommand(MPRemoteCommand *command) { return command == commandCenter.previousTrackCommand || command == commandCenter.nextTrackCommand; } +static BOOL YouModIsSkipCommand(MPRemoteCommand *command) { + MPRemoteCommandCenter *commandCenter = [MPRemoteCommandCenter sharedCommandCenter]; + return command == commandCenter.skipBackwardCommand || command == commandCenter.skipForwardCommand; +} + static BOOL YouModSeekByInterval(NSTimeInterval interval) { if (!IS_ENABLED(ReplacePrevNextButtons) || !YouModCurrentPlayerViewController || !YouModHasPlaybackTime) return NO; if (![YouModCurrentPlayerViewController respondsToSelector:@selector(seekToTime:)]) return NO; @@ -326,6 +331,7 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll proxy.action = action; [YouModRemoteCommandTargetProxies addObject:proxy]; %orig(proxy, @selector(youModHandleRemoteCommandEvent:)); + YouModConfigureRemoteSkipCommands(); return; } @@ -341,12 +347,30 @@ static void YouModAddEndTime(YTPlayerViewController *self, YTSingleVideoControll return handler(event); }; - return %orig(wrappedHandler); + id commandTarget = %orig(wrappedHandler); + YouModConfigureRemoteSkipCommands(); + return commandTarget; } return %orig; } +- (void)setEnabled:(BOOL)enabled { + if (IS_ENABLED(ReplacePrevNextButtons)) { + if (YouModIsPreviousNextCommand(self)) { + %orig(NO); + return; + } + + if (YouModIsSkipCommand(self)) { + %orig(YES); + return; + } + } + + %orig; +} + - (void)removeTarget:(id)target action:(SEL)action { if (YouModIsPreviousNextCommand(self) && YouModRemoteCommandTargetProxies.count > 0) { NSArray *proxies = [YouModRemoteCommandTargetProxies copy];