From a502893c65df44b348b7995f132b39247e79aad3 Mon Sep 17 00:00:00 2001 From: faroukbmiled Date: Thu, 2 Apr 2026 08:10:09 +0100 Subject: [PATCH] inhancing playback for play/pause feature --- control | 2 +- src/Features/Reels/EnhancedPlayback.xm | 237 +++++++++++++++++++++++++ src/Features/Reels/ReelsPlayback.xm | 23 ++- src/InstagramHeaders.h | 7 + src/Tweak.x | 2 +- 5 files changed, 263 insertions(+), 8 deletions(-) create mode 100644 src/Features/Reels/EnhancedPlayback.xm diff --git a/control b/control index d620904..4dd7cc1 100644 --- a/control +++ b/control @@ -1,6 +1,6 @@ Package: com.socuul.scinsta Name: SCInsta -Version: 1.1.3 +Version: 1.1.4 Architecture: iphoneos-arm Description: A feature-rich tweak for Instagram on iOS! Homepage: https://github.com/SoCuul/SCInsta diff --git a/src/Features/Reels/EnhancedPlayback.xm b/src/Features/Reels/EnhancedPlayback.xm new file mode 100644 index 0000000..25b5e0f --- /dev/null +++ b/src/Features/Reels/EnhancedPlayback.xm @@ -0,0 +1,237 @@ +#import "../../Utils.h" +#import "../../InstagramHeaders.h" +#import +#import +#import + +// When tap control is set to pause/play, these enhancements activate: +// - Mute sub-toggle hidden (only play/pause icon visible) +// - Audio forced on in reels tab +// - Play/pause indicator hidden when video plays (fixes IG bug after hold/zoom) +// - Playback toggle synced with overlay visibility during hold/zoom + +static BOOL sciIsPausePlayMode() { + return [[SCIUtils getStringPref:@"reels_tap_control"] isEqualToString:@"pause"]; +} + +static BOOL sciIsInReelsTab = NO; + +// ============ FIND PLAYBACK VIEW ============ +// Handles two IG A/B test variants: +// 1. IGSundialPlaybackToggleView wrapper (contains play + mute subviews) +// 2. Standalone IGDSMediaIconButton (no wrapper) + +static UIView * _Nullable sciFindPlaybackView(UIView *videoCell) { + if (!videoCell) return nil; + Class toggleClass = objc_getClass("IGSundialPlaybackToggle.IGSundialPlaybackToggleView"); + Class iconBtnClass = NSClassFromString(@"IGDSMediaIconButton"); + UIView *fallbackIconBtn = nil; + NSMutableArray *stack = [NSMutableArray arrayWithObject:videoCell]; + for (int d = 0; d < 6; d++) { + NSMutableArray *next = [NSMutableArray array]; + for (UIView *v in stack) { + for (UIView *sub in v.subviews) { + if (toggleClass && [sub isKindOfClass:toggleClass]) return sub; + if (iconBtnClass && [sub isKindOfClass:iconBtnClass] && + sub.frame.size.width > 50 && sub.frame.size.height > 50 && + !sub.hidden && sub.frame.origin.x > 100) { + fallbackIconBtn = sub; + } + [next addObject:sub]; + } + } + stack = next; + } + return fallbackIconBtn; +} + +// ============ KVO: SYNC PLAYBACK VIEW WITH OVERLAY ============ +// IG animates the overlay container via Core Animation during hold/zoom. +// The download button (tag 1337) is inside the container so it hides automatically. +// The playback toggle is in a separate view branch — we KVO the download +// button's layer.opacity and sync the toggle to match. + +@interface SCIOpacityObserver : NSObject +@end + +@implementation SCIOpacityObserver +- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { + if (![keyPath isEqualToString:@"opacity"]) return; + CGFloat opacity = [[change objectForKey:NSKeyValueChangeNewKey] floatValue]; + CALayer *layer = (CALayer *)object; + UIView *dlBtn = (layer.delegate && [layer.delegate isKindOfClass:[UIView class]]) ? (UIView *)layer.delegate : nil; + if (!dlBtn) return; + UIView *videoCell = dlBtn.superview; + while (videoCell && ![NSStringFromClass([videoCell class]) containsString:@"VideoCell"]) + videoCell = videoCell.superview; + UIView *playView = sciFindPlaybackView(videoCell); + if (playView) playView.layer.opacity = opacity; +} +@end + +static SCIOpacityObserver *sciObserver = nil; +static NSHashTable *sciObservedLayers = nil; + +static void sciSetupKVO(UIView *ufi) { + UIView *parent = ufi.superview; + if (!parent) return; + UIView *dlBtn = [parent viewWithTag:1337]; + if (!dlBtn) return; + if (!sciObserver) sciObserver = [[SCIOpacityObserver alloc] init]; + if (!sciObservedLayers) sciObservedLayers = [NSHashTable weakObjectsHashTable]; + if ([sciObservedLayers containsObject:dlBtn.layer]) return; + [dlBtn.layer addObserver:sciObserver forKeyPath:@"opacity" + options:NSKeyValueObservingOptionNew context:NULL]; + [sciObservedLayers addObject:dlBtn.layer]; +} + +// ============ HIDE PLAY INDICATOR WHEN VIDEO PLAYS ============ +// After hold/zoom clear mode, IG resumes but leaves the play indicator visible. +// We hide it when the video actually starts playing or unpauses. + +%hook IGSundialViewerVideoCell +- (void)sundialVideoPlaybackViewDidStartPlaying:(id)view { + %orig; + if (sciIsPausePlayMode()) { + Ivar ivar = class_getInstanceVariable([self class], "_playPauseMediaIndicator"); + if (ivar) { + UIView *indicator = object_getIvar(self, ivar); + if (indicator) indicator.hidden = YES; + } + UIView *playView = sciFindPlaybackView(self); + if (playView) playView.hidden = YES; + } +} + +- (void)videoViewDidUnpause:(id)view { + %orig; + if (sciIsPausePlayMode()) { + Ivar ivar = class_getInstanceVariable([self class], "_playPauseMediaIndicator"); + if (ivar) { + UIView *indicator = object_getIvar(self, ivar); + if (indicator) indicator.hidden = YES; + } + UIView *playView = sciFindPlaybackView(self); + if (playView) playView.hidden = YES; + } +} +%end + +// ============ UFI: SYNC DOWNLOAD BUTTON + SETUP KVO ============ + +%hook IGSundialViewerVerticalUFI +- (void)setAlpha:(CGFloat)alpha { + %orig; + UIView *parent = self.superview; + if (!parent) return; + UIView *dlBtn = [parent viewWithTag:1337]; + if (dlBtn) dlBtn.alpha = alpha; + sciSetupKVO(self); +} +%end + +// ============ HIDE MUTE SUBVIEW IN PLAYBACK TOGGLE ============ +// When pause/play mode is active, hide the mute sub-toggle (top subview) +// and keep only the play/pause button visible. + +static void (*orig_playbackToggle_didMoveToSuperview)(id self, SEL _cmd); +static void (*orig_playbackToggle_layoutSubviews)(id self, SEL _cmd); + +static void sciHideMuteSubview(UIView *toggleView) { + if (!sciIsPausePlayMode()) { + for (UIView *sub in toggleView.subviews) { + if (sub.tag == 9999) { + sub.hidden = NO; sub.alpha = 1; sub.userInteractionEnabled = YES; sub.tag = 0; + } + } + return; + } + NSArray *subs = toggleView.subviews; + if (subs.count < 2) return; + UIView *topView = nil; + CGFloat minY = CGFLOAT_MAX; + int visibleCount = 0; + for (UIView *sub in subs) { + if (sub.frame.size.width < 1 || sub.frame.size.height < 1) continue; + visibleCount++; + if (sub.frame.origin.y < minY) { minY = sub.frame.origin.y; topView = sub; } + } + if (topView && visibleCount >= 2) { + topView.hidden = YES; topView.alpha = 0; topView.userInteractionEnabled = NO; topView.tag = 9999; + } +} + +static void new_playbackToggle_didMoveToSuperview(id self, SEL _cmd) { + orig_playbackToggle_didMoveToSuperview(self, _cmd); + sciHideMuteSubview((UIView *)self); +} +static void new_playbackToggle_layoutSubviews(id self, SEL _cmd) { + orig_playbackToggle_layoutSubviews(self, _cmd); + sciHideMuteSubview((UIView *)self); +} + +// ============ FORCE AUDIO IN REELS TAB ============ +// Uses _didTapSoundButton on the section controller — the same code path +// as physically tapping the sound button. + +static void sciForceUnmuteCell(id videoCell) { + if (!videoCell) return; + Ivar delegateIvar = class_getInstanceVariable([videoCell class], "_delegate"); + if (!delegateIvar) return; + id sectionCtrl = object_getIvar(videoCell, delegateIvar); + if (!sectionCtrl) return; + SEL isAudioSel = NSSelectorFromString(@"isAudioEnabled"); + if (![sectionCtrl respondsToSelector:isAudioSel]) return; + BOOL audioOn = ((BOOL(*)(id,SEL))objc_msgSend)(sectionCtrl, isAudioSel); + if (audioOn) return; + SEL tapSel = NSSelectorFromString(@"_didTapSoundButton"); + if ([sectionCtrl respondsToSelector:tapSel]) { + ((void(*)(id,SEL))objc_msgSend)(sectionCtrl, tapSel); + } +} + +%hook IGSundialFeedViewController +- (void)viewDidAppear:(BOOL)animated { + %orig; + sciIsInReelsTab = YES; + // Force unmute first reel — retry until the cell is ready + if (sciIsPausePlayMode()) { + id feedVC = self; + for (int i = 0; i < 10; i++) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((0.1 + i * 0.15) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ + if (!sciIsInReelsTab) return; + SEL sel = NSSelectorFromString(@"_currentAudioCell"); + if (![feedVC respondsToSelector:sel]) return; + id cell = ((id(*)(id,SEL))objc_msgSend)(feedVC, sel); + if (cell) sciForceUnmuteCell(cell); + }); + } + } +} +- (void)viewWillDisappear:(BOOL)animated { + %orig; + sciIsInReelsTab = NO; +} +- (void)scrollViewDidEndDecelerating:(UIScrollView *)scrollView { + %orig; + if (sciIsPausePlayMode() && sciIsInReelsTab) { + SEL sel = NSSelectorFromString(@"_currentAudioCell"); + if ([self respondsToSelector:sel]) { + id cell = ((id(*)(id,SEL))objc_msgSend)(self, sel); + if (cell) sciForceUnmuteCell(cell); + } + } +} +%end + +// ============ RUNTIME HOOKS ============ + +%ctor { + Class toggleClass = objc_getClass("IGSundialPlaybackToggle.IGSundialPlaybackToggleView"); + if (toggleClass) { + MSHookMessageEx(toggleClass, @selector(didMoveToSuperview), + (IMP)new_playbackToggle_didMoveToSuperview, (IMP *)&orig_playbackToggle_didMoveToSuperview); + MSHookMessageEx(toggleClass, @selector(layoutSubviews), + (IMP)new_playbackToggle_layoutSubviews, (IMP *)&orig_playbackToggle_layoutSubviews); + } +} diff --git a/src/Features/Reels/ReelsPlayback.xm b/src/Features/Reels/ReelsPlayback.xm index 9919ddb..eebee89 100644 --- a/src/Features/Reels/ReelsPlayback.xm +++ b/src/Features/Reels/ReelsPlayback.xm @@ -52,13 +52,10 @@ } %end -// * Disable volume/mute button triggering unmutes +// * Disable auto-unmuting reels +// Blocks all paths that can unmute: hardware buttons, headphones, +// mute switch, and the audio state announcer. %hook IGAudioStatusAnnouncer -- (void)_muteSwitchStateChanged:(id)changed { - if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) { - %orig(changed); - } -} - (void)_didPressVolumeButton:(id)button { if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) { %orig(button); @@ -69,4 +66,18 @@ %orig(headphones); } } +- (void)_muteSwitchStateChanged:(id)changed { + if (![SCIUtils getBoolPref:@"disable_auto_unmuting_reels"]) { + %orig(changed); + } +} +// Block the announcer from broadcasting "audio enabled" state changes +- (void)_announceForDeviceStateChangesIfNeededForAudioEnabled:(BOOL)enabled reason:(NSInteger)reason { + // When pause/play mode is on, allow unmute (our force-unmute needs this path) + BOOL pausePlayMode = [[SCIUtils getStringPref:@"reels_tap_control"] isEqualToString:@"pause"]; + if ([SCIUtils getBoolPref:@"disable_auto_unmuting_reels"] && enabled && !pausePlayMode) { + return; + } + %orig; +} %end \ No newline at end of file diff --git a/src/InstagramHeaders.h b/src/InstagramHeaders.h index 90fa0f6..2eac72a 100644 --- a/src/InstagramHeaders.h +++ b/src/InstagramHeaders.h @@ -395,6 +395,10 @@ @property(nonatomic, copy, readwrite) NSArray *tools; @end +// Swift classes: IGSundialPlaybackToggle.IGSundialPlaybackToggleView +// IGSundialClearMode.IGSundialClearedOverlayView +// Hooked via %hook with mangled names — see EnhancedPlayback.xm + @interface IGSundialViewerVerticalUFI : UIView - (void)_didTapLikeButton:(id)arg1; - (void)_didTapRepostButton:(id)arg1; @@ -404,6 +408,9 @@ - (id)tabStringFromSurfaceIntent; @end +@interface IGSundialViewerVideoSectionController : NSObject +@end + @interface IGSundialFeedViewController : UIViewController - (void)refreshControlDidEndFinishLoadingAnimation:(id)arg1; @end diff --git a/src/Tweak.x b/src/Tweak.x index 54a4446..af8d5ff 100644 --- a/src/Tweak.x +++ b/src/Tweak.x @@ -13,7 +13,7 @@ /////////////////////////////////////////////////////////// // * Tweak version * -NSString *SCIVersionString = @"v1.1.3"; +NSString *SCIVersionString = @"v1.1.4"; // Variables that work across features BOOL dmVisualMsgsViewedButtonEnabled = false;