From d4af0f1ee023fc7623580391904de3b1002a2f7e Mon Sep 17 00:00:00 2001 From: inotia00 <108592928+inotia00@users.noreply.github.com> Date: Fri, 14 Mar 2025 18:18:53 +0900 Subject: [PATCH] feat(YouTube Music): Add `Spoof player parameter` patch https://github.com/inotia00/ReVanced_Extended/issues/2832 --- .../misc/SpoofPlayerParameterPatch.java | 174 ++++++++++++++++++ .../extension/music/settings/Settings.java | 1 + .../music/shared/VideoInformation.java | 72 ++++++++ .../music/misc/album/AlbumMusicVideoPatch.kt | 9 +- .../music/utils/fix/parameter/Fingerprints.kt | 26 +++ .../parameter/SpoofPlayerParameterPatch.kt | 82 +++++++++ .../patches/music/utils/patch/PatchList.kt | 4 + .../information/VideoInformationPatch.kt | 21 ++- .../video/playerresponse/Fingerprints.kt | 5 +- .../PlayerResponseMethodHookPatch.kt | 87 ++++++++- .../music/settings/host/values/strings.xml | 5 + 11 files changed, 469 insertions(+), 17 deletions(-) create mode 100644 extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt create mode 100644 patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java new file mode 100644 index 000000000..d5e5b014b --- /dev/null +++ b/extensions/shared/src/main/java/app/revanced/extension/music/patches/misc/SpoofPlayerParameterPatch.java @@ -0,0 +1,174 @@ +package app.revanced.extension.music.patches.misc; + +import static app.revanced.extension.music.shared.VideoInformation.parameterIsAgeRestricted; +import static app.revanced.extension.music.shared.VideoInformation.parameterIsSample; + +import androidx.annotation.GuardedBy; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import org.apache.commons.lang3.BooleanUtils; + +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import app.revanced.extension.music.settings.Settings; +import app.revanced.extension.music.shared.VideoInformation; +import app.revanced.extension.shared.utils.Logger; + +@SuppressWarnings("unused") +public class SpoofPlayerParameterPatch { + /** + * Used in YouTube Music. + */ + private static final boolean SPOOF_PLAYER_PARAMETER = Settings.SPOOF_PLAYER_PARAMETER.get(); + + /** + * Parameter to fix playback issues. + * Used in YouTube Music Samples. + */ + private static final String PLAYER_PARAMETER_SAMPLES = + "8AEB2AUBogYVAUY4C8W9wrM-FdhjSW4MnCgH44uhkAcI"; + + /** + * Parameter to fix playback issues. + * Used in YouTube Shorts. + */ + private static final String PLAYER_PARAMETER_SHORTS = + "8AEByAMkuAQ0ogYVAePzwRN3uesV1sPI2x4-GkDYlvqUkAcC"; + + /** + * On app first start, the first video played usually contains a single non-default window setting value + * and all other subtitle settings for the video are (incorrect) default Samples window settings. + * For this situation, the Samples settings must be replaced. + *

+ * But some videos use multiple text positions on screen (such as youtu.be/3hW1rMNC89o), + * and by chance many of the subtitles uses window positions that match a default Samples position. + * To handle these videos, selectively allowing the Samples specific window settings to 'pass thru' unchanged, + * but only if the video contains multiple non-default subtitle window positions. + *

+ * Do not enable 'pass thru mode' until this many non default subtitle settings are observed for a single video. + */ + private static final int NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU = 2; + + /** + * The number of non default subtitle settings encountered for the current video. + */ + private static int numberOfNonDefaultSettingsObserved; + + @GuardedBy("itself") + private static final Map lastVideoIds = new LinkedHashMap<>() { + private static final int NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK = 5; + + @Override + protected boolean removeEldestEntry(Entry eldest) { + return size() > NUMBER_OF_LAST_VIDEO_IDS_TO_TRACK; + } + }; + + /** + * Injection point. + */ + public static String spoofParameter(@NonNull String videoId, @Nullable String parameter) { + if (SPOOF_PLAYER_PARAMETER) { + synchronized (lastVideoIds) { + Boolean isSamples = parameterIsSample(parameter); + if (lastVideoIds.put(videoId, isSamples) == null) { + Logger.printDebug(() -> "New video loaded (videoId: " + videoId + ", isSamples: " + isSamples + ")"); + } + } + return parameterIsAgeRestricted(parameter) + ? PLAYER_PARAMETER_SHORTS + : PLAYER_PARAMETER_SAMPLES; + } + return parameter; + } + + /** + * Injection point. Overrides values passed into SubtitleWindowSettings constructor. + * + * @param ap anchor position. A bitmask with 6 bit fields, that appears to indicate the layout position on screen + * @param ah anchor horizontal. A percentage [0, 100], that appears to be a horizontal text anchor point + * @param av anchor vertical. A percentage [0, 100], that appears to be a vertical text anchor point + * @param vs appears to indicate if subtitles exist, and the value is always true. + * @param sd function is not entirely clear + */ + public static int[] fixSubtitleWindowPosition(int ap, int ah, int av, boolean vs, boolean sd) { + // Videos with custom captions that specify screen positions appear to always have correct screen positions (even with spoofing). + // But for auto generated and most other captions, the spoof incorrectly gives various default Samples caption settings. + // Check for these known default Samples captions parameters, and replace with the known correct values. + // + // If a regular video uses a custom subtitle setting that match a default Samples setting, + // then this will incorrectly replace the setting. + // But, if the video uses multiple subtitles in different screen locations, then detect the non-default values + // and do not replace any window settings for the video (regardless if they match a Samples default). + if (SPOOF_PLAYER_PARAMETER && + numberOfNonDefaultSettingsObserved < NUMBER_OF_NON_DEFAULT_SUBTITLES_BEFORE_ENABLING_PASSTHRU) { + synchronized (lastVideoIds) { + String videoId = VideoInformation.getVideoId(); + Boolean isSample = lastVideoIds.get(videoId); + if (BooleanUtils.isFalse(isSample)) { + for (SubtitleWindowReplacementSettings setting : SubtitleWindowReplacementSettings.values()) { + if (setting.match(ap, ah, av, vs, sd)) { + return setting.replacementSetting(); + } + } + + numberOfNonDefaultSettingsObserved++; + } + } + } + + return new int[]{ap, ah, av}; + } + + /** + * Injection point. + *

+ * Return false to force disable age restricted playback feature flag. + */ + public static boolean forceDisableAgeRestrictedPlaybackFeatureFlag(boolean original) { + if (SPOOF_PLAYER_PARAMETER) { + return false; + } + return original; + } + + /** + * Known incorrect default Samples subtitle parameters, and the corresponding correct (non-Samples) values. + */ + private enum SubtitleWindowReplacementSettings { + DEFAULT_SAMPLES_PARAMETERS_1(10, 50, 0, true, false, + 34, 50, 95), + DEFAULT_SAMPLES_PARAMETERS_2(9, 20, 0, true, false, + 34, 50, 90), + DEFAULT_SAMPLES_PARAMETERS_3(9, 20, 0, true, true, + 33, 20, 100); + + // original values + final int ap, ah, av; + final boolean vs, sd; + + // replacement int values + final int[] replacement; + + SubtitleWindowReplacementSettings(int ap, int ah, int av, boolean vs, boolean sd, + int replacementAp, int replacementAh, int replacementAv) { + this.ap = ap; + this.ah = ah; + this.av = av; + this.vs = vs; + this.sd = sd; + this.replacement = new int[]{replacementAp, replacementAh, replacementAv}; + } + + boolean match(int ap, int ah, int av, boolean vs, boolean sd) { + return this.ap == ap && this.ah == ah && this.av == av && this.vs == vs && this.sd == sd; + } + + int[] replacementSetting() { + return replacement; + } + } +} diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java index b0b531d8b..75b8a6c61 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/settings/Settings.java @@ -190,6 +190,7 @@ public class Settings extends BaseSettings { public static final BooleanSetting DISABLE_DRC_AUDIO = new BooleanSetting("revanced_disable_drc_audio", FALSE, true); public static final BooleanSetting DISABLE_MUSIC_VIDEO_IN_ALBUM = new BooleanSetting("revanced_disable_music_video_in_album", FALSE, true); public static final EnumSetting DISABLE_MUSIC_VIDEO_IN_ALBUM_REDIRECT_TYPE = new EnumSetting<>("revanced_disable_music_video_in_album_redirect_type", RedirectType.REDIRECT, true); + public static final BooleanSetting SPOOF_PLAYER_PARAMETER = new BooleanSetting("revanced_spoof_player_parameter", TRUE, true); public static final BooleanSetting SETTINGS_IMPORT_EXPORT = new BooleanSetting("revanced_extended_settings_import_export", FALSE, false); // PreferenceScreen: Return YouTube Dislike diff --git a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java index 12ce65258..451d873bf 100644 --- a/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java +++ b/extensions/shared/src/main/java/app/revanced/extension/music/shared/VideoInformation.java @@ -22,12 +22,25 @@ public final class VideoInformation { private static final float DEFAULT_YOUTUBE_MUSIC_PLAYBACK_SPEED = 1.0f; private static final int DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY = -2; private static final String DEFAULT_YOUTUBE_MUSIC_VIDEO_QUALITY_STRING = getString("quality_auto"); + /** + * Prefix present in all Age-restricted music player parameters signature. + */ + private static final String AGE_RESTRICTED_PLAYER_PARAMETER = "ygYQ"; + /** + * Prefix present in all Sample player parameters signature. + */ + private static final String SAMPLES_PLAYER_PARAMETERS = "8AEB"; + @NonNull private static String videoId = ""; private static long videoLength = 0; private static long videoTime = -1; + @NonNull + private static volatile String playerResponseVideoId = ""; + private static volatile boolean playerResponseVideoIdIsSample; + /** * The current playback speed */ @@ -85,6 +98,65 @@ public final class VideoInformation { videoId = newlyLoadedVideoId; } + /** + * Differs from {@link #videoId} as this is the video id for the + * last player response received, which may not be the last video opened. + *

+ * If Shorts are loading the background, this commonly will be + * different from the Short that is currently on screen. + *

+ * For most use cases, you should instead use {@link #getVideoId()}. + * + * @return The id of the last video loaded, or an empty string if no videos have been loaded yet. + */ + @NonNull + public static String getPlayerResponseVideoId() { + return playerResponseVideoId; + } + + /** + * @return If the last player response video id was a Sample. + */ + public static boolean lastPlayerResponseIsSample() { + return playerResponseVideoIdIsSample; + } + + /** + * Injection point. Called off the main thread. + * + * @param videoId The id of the last video loaded. + */ + public static void setPlayerResponseVideoId(@NonNull String videoId) { + if (!playerResponseVideoId.equals(videoId)) { + playerResponseVideoId = videoId; + } + } + + /** + * @return If the player parameter is for a Age-restricted video. + */ + public static boolean parameterIsAgeRestricted(@Nullable String parameter) { + return parameter != null && parameter.startsWith(AGE_RESTRICTED_PLAYER_PARAMETER); + } + + /** + * @return If the player parameter is for a Sample. + */ + public static boolean parameterIsSample(@Nullable String parameter) { + return parameter != null && parameter.startsWith(SAMPLES_PLAYER_PARAMETERS); + } + + /** + * Injection point. + */ + @Nullable + public static String newPlayerResponseParameter(@NonNull String videoId, @Nullable String playerParameter) { + playerResponseVideoIdIsSample = parameterIsSample(playerParameter); + Logger.printDebug(() -> "videoId: " + videoId + ", playerParameter: " + playerParameter); + + return playerParameter; // Return the original value since we are observing and not modifying. + } + /** * Seek on the current video. * Does not function for playback of Shorts. diff --git a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt index f72a914ee..5944b88cd 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/misc/album/AlbumMusicVideoPatch.kt @@ -14,7 +14,8 @@ import app.revanced.patches.music.utils.settings.addSwitchPreference import app.revanced.patches.music.utils.settings.settingsPatch import app.revanced.patches.music.video.information.videoIdHook import app.revanced.patches.music.video.information.videoInformationPatch -import app.revanced.patches.music.video.playerresponse.hookPlayerResponse +import app.revanced.patches.music.video.playerresponse.Hook +import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch import app.revanced.util.findMethodOrThrow import app.revanced.util.fingerprint.methodOrThrow @@ -46,7 +47,11 @@ val albumMusicVideoPatch = bytecodePatch( // region hook player response - hookPlayerResponse("$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V") + addPlayerResponseMethodHook( + Hook.VideoIdAndPlaylistId( + "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponse(Ljava/lang/String;Ljava/lang/String;I)V" + ), + ) // endregion diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt new file mode 100644 index 000000000..727df6df9 --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/Fingerprints.kt @@ -0,0 +1,26 @@ +package app.revanced.patches.music.utils.fix.parameter + +import app.revanced.util.fingerprint.legacyFingerprint +import app.revanced.util.or +import com.android.tools.smali.dexlib2.AccessFlags + +internal val subtitleWindowFingerprint = legacyFingerprint( + name = "subtitleWindowFingerprint", + returnType = "V", + accessFlags = AccessFlags.PUBLIC or AccessFlags.CONSTRUCTOR, + parameters = listOf("I", "I", "I", "Z", "Z"), + strings = listOf("invalid anchorHorizontalPos: %s"), +) + +/** + * If this flag is activated, a playback issue occurs in age-restricted videos. + */ +internal const val AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG = 45651506L + +internal val ageRestrictedPlaybackFeatureFlagFingerprint = legacyFingerprint( + name = "ageRestrictedPlaybackFeatureFlagFingerprint", + returnType = "Z", + accessFlags = AccessFlags.PUBLIC or AccessFlags.FINAL, + parameters = emptyList(), + literals = listOf(AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG), +) diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt new file mode 100644 index 000000000..5e71c60cd --- /dev/null +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/fix/parameter/SpoofPlayerParameterPatch.kt @@ -0,0 +1,82 @@ +package app.revanced.patches.music.utils.fix.parameter + +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions +import app.revanced.patcher.patch.bytecodePatch +import app.revanced.patches.music.utils.compatibility.Constants.COMPATIBLE_PACKAGE +import app.revanced.patches.music.utils.extension.Constants.MISC_PATH +import app.revanced.patches.music.utils.patch.PatchList.SPOOF_PLAYER_PARAMETER +import app.revanced.patches.music.utils.settings.CategoryType +import app.revanced.patches.music.utils.settings.ResourceUtils.updatePatchStatus +import app.revanced.patches.music.utils.settings.addSwitchPreference +import app.revanced.patches.music.utils.settings.settingsPatch +import app.revanced.patches.music.video.information.videoInformationPatch +import app.revanced.patches.music.video.playerresponse.Hook +import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook +import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch +import app.revanced.util.fingerprint.injectLiteralInstructionBooleanCall +import app.revanced.util.fingerprint.methodOrThrow +import app.revanced.util.fingerprint.resolvable + +private const val EXTENSION_CLASS_DESCRIPTOR = + "$MISC_PATH/SpoofPlayerParameterPatch;" + +@Suppress("unused") +val spoofPlayerParameterPatch = bytecodePatch( + SPOOF_PLAYER_PARAMETER.title, + SPOOF_PLAYER_PARAMETER.summary +) { + compatibleWith(COMPATIBLE_PACKAGE) + + dependsOn( + settingsPatch, + videoInformationPatch, + playerResponseMethodHookPatch, + ) + + execute { + + addPlayerResponseMethodHook( + Hook.PlayerParameter( + "$EXTENSION_CLASS_DESCRIPTOR->spoofParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" + ), + ) + + // region fix for subtitles position + + subtitleWindowFingerprint.methodOrThrow().addInstructions( + 0, + """ + invoke-static {p1, p2, p3, p4, p5}, $EXTENSION_CLASS_DESCRIPTOR->fixSubtitleWindowPosition(IIIZZ)[I + move-result-object v0 + const/4 v1, 0x0 + aget p1, v0, v1 # ap, anchor position + const/4 v1, 0x1 + aget p2, v0, v1 # ah, horizontal anchor + const/4 v1, 0x2 + aget p3, v0, v1 # av, vertical anchor + """ + ) + + // endregion + + // region fix for feature flags + + if (ageRestrictedPlaybackFeatureFlagFingerprint.resolvable()) { + ageRestrictedPlaybackFeatureFlagFingerprint.injectLiteralInstructionBooleanCall( + AGE_RESTRICTED_PLAYBACK_FEATURE_FLAG, + "$EXTENSION_CLASS_DESCRIPTOR->forceDisableAgeRestrictedPlaybackFeatureFlag(Z)Z" + ) + } + + // endregion + + addSwitchPreference( + CategoryType.MISC, + "revanced_spoof_player_parameter", + "true" + ) + + updatePatchStatus(SPOOF_PLAYER_PARAMETER) + + } +} diff --git a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt index 1b9c330e0..7fb107952 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/utils/patch/PatchList.kt @@ -157,6 +157,10 @@ internal enum class PatchList( "Spoof client", "Adds options to spoof the client to allow playback." ), + SPOOF_PLAYER_PARAMETER( + "Spoof player parameter", + "Adds options to spoof player parameter to allow playback." + ), TRANSLATIONS_FOR_YOUTUBE_MUSIC( "Translations for YouTube Music", "Add translations or remove string resources." diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt index 275e96b4c..bc4b331aa 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/information/VideoInformationPatch.kt @@ -13,6 +13,9 @@ import app.revanced.patches.music.utils.extension.Constants.SHARED_PATH import app.revanced.patches.music.utils.playbackSpeedFingerprint import app.revanced.patches.music.utils.playbackSpeedParentFingerprint import app.revanced.patches.music.utils.resourceid.sharedResourceIdPatch +import app.revanced.patches.music.video.playerresponse.Hook +import app.revanced.patches.music.video.playerresponse.addPlayerResponseMethodHook +import app.revanced.patches.music.video.playerresponse.playerResponseMethodHookPatch import app.revanced.patches.shared.mdxPlayerDirectorSetVideoStageFingerprint import app.revanced.patches.shared.videoLengthFingerprint import app.revanced.util.addStaticFieldToExtension @@ -71,7 +74,10 @@ private var videoTimeConstructorInsertIndex = 2 val videoInformationPatch = bytecodePatch( description = "videoInformationPatch", ) { - dependsOn(sharedResourceIdPatch) + dependsOn( + playerResponseMethodHookPatch, + sharedResourceIdPatch + ) execute { fun addSeekInterfaceMethods( @@ -241,7 +247,18 @@ val videoInformationPatch = bytecodePatch( * Set current video id */ videoIdHook("$EXTENSION_CLASS_DESCRIPTOR->setVideoId(Ljava/lang/String;)V") - + addPlayerResponseMethodHook( + Hook.VideoId( + "$EXTENSION_CLASS_DESCRIPTOR->setPlayerResponseVideoId(Ljava/lang/String;)V" + ), + ) + // Call before any other video id hooks, + // so they can use VideoInformation and check if the video id is for a Short. + addPlayerResponseMethodHook( + Hook.PlayerParameterBeforeVideoId( + "$EXTENSION_CLASS_DESCRIPTOR->newPlayerResponseParameter(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;" + ) + ) /** * Hook current playback speed */ diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt index 450ce37ea..1208c5e2b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/Fingerprints.kt @@ -11,8 +11,7 @@ private val PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST = listOf( "[B", "Ljava/lang/String;", // Player parameters proto buffer. "Ljava/lang/String;", // PlaylistId. - "I", // PlaylistIndex. - "I" + "I" // PlaylistIndex. ) /** @@ -30,7 +29,7 @@ internal val playerParameterBuilderFingerprint = legacyFingerprint( return@custom false } - val startsWithMethodParameterList = parameterTypes.slice(0..5) + val startsWithMethodParameterList = parameterTypes.slice(0..4) parametersEqual( PLAYER_PARAMETER_STARTS_WITH_PARAMETER_LIST, diff --git a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt index 8570097d0..85e51a62b 100644 --- a/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt +++ b/patches/src/main/kotlin/app/revanced/patches/music/video/playerresponse/PlayerResponseMethodHookPatch.kt @@ -1,22 +1,35 @@ package app.revanced.patches.music.video.playerresponse import app.revanced.patcher.extensions.InstructionExtensions.addInstruction +import app.revanced.patcher.extensions.InstructionExtensions.addInstructions import app.revanced.patcher.patch.bytecodePatch import app.revanced.patcher.util.proxy.mutableTypes.MutableMethod +import app.revanced.patches.music.utils.extension.sharedExtensionPatch import app.revanced.patches.music.utils.playservice.is_7_03_or_greater import app.revanced.patches.music.utils.playservice.versionCheckPatch import app.revanced.util.fingerprint.methodOrThrow +private val hooks = mutableSetOf() + +fun addPlayerResponseMethodHook(hook: Hook) { + hooks += hook +} + private const val REGISTER_VIDEO_ID = "p1" +private const val REGISTER_PLAYER_PARAMETER = "p3" private const val REGISTER_PLAYLIST_ID = "p4" private const val REGISTER_PLAYLIST_INDEX = "p5" private lateinit var playerResponseMethod: MutableMethod +private var numberOfInstructionsAdded = 0 val playerResponseMethodHookPatch = bytecodePatch( description = "playerResponseMethodHookPatch" ) { - dependsOn(versionCheckPatch) + dependsOn( + sharedExtensionPatch, + versionCheckPatch, + ) execute { playerResponseMethod = if (is_7_03_or_greater) { @@ -25,16 +38,70 @@ val playerResponseMethodHookPatch = bytecodePatch( playerParameterBuilderLegacyFingerprint }.methodOrThrow() } + + finalize { + fun hookVideoId(hook: Hook) { + playerResponseMethod.addInstruction( + 0, + "invoke-static {$REGISTER_VIDEO_ID}, $hook", + ) + numberOfInstructionsAdded++ + } + + fun hookVideoIdAndPlaylistId(hook: Hook) { + playerResponseMethod.addInstruction( + 0, + "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYLIST_ID, $REGISTER_PLAYLIST_INDEX}, $hook", + ) + numberOfInstructionsAdded++ + } + + fun hookPlayerParameter(hook: Hook) { + playerResponseMethod.addInstructions( + 0, + """ + invoke-static {$REGISTER_VIDEO_ID, v0}, $hook + move-result-object v0 + """, + ) + numberOfInstructionsAdded += 2 + } + + // Reverse the order in order to preserve insertion order of the hooks. + val beforeVideoIdHooks = + hooks.filterIsInstance().asReversed() + val videoIdHooks = hooks.filterIsInstance().asReversed() + val videoIdAndPlaylistIdHooks = hooks.filterIsInstance().asReversed() + val afterVideoIdHooks = hooks.filterIsInstance().asReversed() + + // Add the hooks in this specific order as they insert instructions at the beginning of the method. + afterVideoIdHooks.forEach(::hookPlayerParameter) + videoIdAndPlaylistIdHooks.forEach(::hookVideoIdAndPlaylistId) + videoIdHooks.forEach(::hookVideoId) + beforeVideoIdHooks.forEach(::hookPlayerParameter) + + playerResponseMethod.apply { + addInstruction( + 0, + "move-object/from16 v0, $REGISTER_PLAYER_PARAMETER" + ) + numberOfInstructionsAdded++ + + // Move the modified register back. + addInstruction( + numberOfInstructionsAdded, + "move-object/from16 $REGISTER_PLAYER_PARAMETER, v0" + ) + } + } } -fun hookPlayerResponse( - descriptor: String, - onlyVideoId: Boolean = false -) { - val smaliInstruction = if (onlyVideoId) - "invoke-static {$REGISTER_VIDEO_ID}, $descriptor" - else - "invoke-static {$REGISTER_VIDEO_ID, $REGISTER_PLAYLIST_ID, $REGISTER_PLAYLIST_INDEX}, $descriptor" +sealed class Hook(private val methodDescriptor: String) { + class VideoId(methodDescriptor: String) : Hook(methodDescriptor) + class VideoIdAndPlaylistId(methodDescriptor: String) : Hook(methodDescriptor) - playerResponseMethod.addInstruction(0, smaliInstruction) + class PlayerParameter(methodDescriptor: String) : Hook(methodDescriptor) + class PlayerParameterBeforeVideoId(methodDescriptor: String) : Hook(methodDescriptor) + + override fun toString() = methodDescriptor } diff --git a/patches/src/main/resources/music/settings/host/values/strings.xml b/patches/src/main/resources/music/settings/host/values/strings.xml index c8a34ae88..01a58ec1d 100644 --- a/patches/src/main/resources/music/settings/host/values/strings.xml +++ b/patches/src/main/resources/music/settings/host/values/strings.xml @@ -501,6 +501,11 @@ Info: Android Music 5.29.53 iOS Music 6.21 iOS Music 7.04 + Spoof player parameter + "Spoof the player parameter to prevent playback issues. + +Side effect: +• Sometimes the subtitles are located at the top of the player instead of the bottom." Watch history type "• Original: Follows the watch history settings of Google account, but watch history may not work due to DNS or VPN. • Replace domain: Follows the watch history settings of Google account.