diff --git a/PlayTools.xcodeproj/project.pbxproj b/PlayTools.xcodeproj/project.pbxproj index 6c0f2ad5..e8d919f9 100644 --- a/PlayTools.xcodeproj/project.pbxproj +++ b/PlayTools.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ B46C02C72C634AB5007637AB /* BatteryLevel.m in Sources */ = {isa = PBXBuildFile; fileRef = B46C02C62C634AB5007637AB /* BatteryLevel.m */; }; B66E65002C936EA100E48FD0 /* PlayedAppleDBConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = B66E64FF2C936E9800E48FD0 /* PlayedAppleDBConstants.swift */; }; B6D774FF2ACFC3D900C0D9D8 /* SwordRPC in Frameworks */ = {isa = PBXBuildFile; productRef = B6D774FE2ACFC3D900C0D9D8 /* SwordRPC */; }; + D04B12CA2EF6F3DD008FEC14 /* MediaVolume.m in Sources */ = {isa = PBXBuildFile; fileRef = D04B12C92EF6F3DD008FEC14 /* MediaVolume.m */; }; EEB248592B81D074000C230A /* PlayedAppleDB.swift in Sources */ = {isa = PBXBuildFile; fileRef = EEB248582B81D074000C230A /* PlayedAppleDB.swift */; }; /* End PBXBuildFile section */ @@ -183,6 +184,8 @@ B46C02C62C634AB5007637AB /* BatteryLevel.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = BatteryLevel.m; sourceTree = ""; }; B46C02C82C634C60007637AB /* BatteryLevel.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = BatteryLevel.h; sourceTree = ""; }; B66E64FF2C936E9800E48FD0 /* PlayedAppleDBConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedAppleDBConstants.swift; sourceTree = ""; }; + D04B12C92EF6F3DD008FEC14 /* MediaVolume.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MediaVolume.m; sourceTree = ""; }; + D04B12CC2EF6F3F9008FEC14 /* MediaVolume.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MediaVolume.h; sourceTree = ""; }; EEB248582B81D074000C230A /* PlayedAppleDB.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PlayedAppleDB.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -480,6 +483,8 @@ 6E7663A428D0FEBE00DE4AF9 /* AKPluginLoader.swift */, B46C02C62C634AB5007637AB /* BatteryLevel.m */, B46C02C82C634C60007637AB /* BatteryLevel.h */, + D04B12C92EF6F3DD008FEC14 /* MediaVolume.m */, + D04B12CC2EF6F3F9008FEC14 /* MediaVolume.h */, ); path = Utils; sourceTree = ""; @@ -729,6 +734,7 @@ AA71970D287A44D200623C15 /* PlaySettings.swift in Sources */, 95D474FB2C0BA77B0072797F /* JoystickElement.swift in Sources */, AA719759287A480D00623C15 /* PlayAction.swift in Sources */, + D04B12CA2EF6F3DD008FEC14 /* MediaVolume.m in Sources */, 954389C82B392D5300B063BB /* Button.swift in Sources */, 6E7663A528D0FEBE00DE4AF9 /* AKPluginLoader.swift in Sources */, 9562D16F2AB50D34002C329D /* ActionDispatcher.swift in Sources */, diff --git a/PlayTools/Utils/MediaVolume.h b/PlayTools/Utils/MediaVolume.h new file mode 100644 index 00000000..e73a5c0a --- /dev/null +++ b/PlayTools/Utils/MediaVolume.h @@ -0,0 +1,18 @@ +// +// MediaVolume.h +// PlayTools +// +// Created by Undefined on 2025/12/20. +// + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface NSObject (SwizzleVolume) + +- (void)swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector; + +@end + +NS_ASSUME_NONNULL_END diff --git a/PlayTools/Utils/MediaVolume.m b/PlayTools/Utils/MediaVolume.m new file mode 100644 index 00000000..29bccfb2 --- /dev/null +++ b/PlayTools/Utils/MediaVolume.m @@ -0,0 +1,75 @@ +// +// MediaVolume.m +// PlayTools +// +// Created by Undefined on 2025/12/20. +// + +#import +#import +#import +#import "MediaVolume.h" + +__attribute__((visibility("hidden"))) +@interface MediaVolumeLoader : NSObject +@end + +@implementation NSObject (SwizzleVolume) + +- (void)swizzleInstanceMethod:(SEL)origSelector withMethod:(SEL)newSelector { + Class cls = [self class]; + // If current class doesn't exist selector, then get super + Method originalMethod = class_getInstanceMethod(cls, origSelector); + Method swizzledMethod = class_getInstanceMethod(cls, newSelector); + + // Add selector if it doesn't exist, implement append with method + if (class_addMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod))) { + // Replace class instance method, added if selector not exist + // For class cluster, it always adds new selector here + class_replaceMethod(cls, + newSelector, + method_getImplementation(originalMethod), + method_getTypeEncoding(originalMethod)); + } else { + // SwizzleMethod maybe belongs to super + class_replaceMethod(cls, + newSelector, + class_replaceMethod(cls, + origSelector, + method_getImplementation(swizzledMethod), + method_getTypeEncoding(swizzledMethod)), + method_getTypeEncoding(originalMethod)); + } +} + +- (float)pm_get_volume { + return 0.3f; +} + +- (void)pm_set_volume:(float)value { + return; +} + +- (void)pm_set_volume:(float)value animated:(BOOL)animated { + return; +} + +@end + +@implementation MediaVolumeLoader + ++ (void)load { + [objc_getClass("AVAudioSession") swizzleInstanceMethod:@selector(outputVolume) withMethod:@selector(pm_get_volume)]; + + [objc_getClass("MPVolumeSlider") swizzleInstanceMethod:@selector(value) withMethod:@selector(pm_get_volume)]; + [objc_getClass("MPVolumeSlider") swizzleInstanceMethod:@selector(setValue:) withMethod:@selector(pm_set_volume:)]; + [objc_getClass("MPVolumeSlider") swizzleInstanceMethod:@selector(setValue:animated:) withMethod:@selector(pm_set_volume:animated:)]; + + [objc_getClass("MPMusicPlayerController") swizzleInstanceMethod:@selector(volume) withMethod:@selector(pm_get_volume)]; + [objc_getClass("MPMusicPlayerController") swizzleInstanceMethod:@selector(setVolume:) withMethod:@selector(pm_set_volume:)]; +} + +@end